"""
Management of Zabbix users.

:codeauthor: Jiri Kotlin <jiri.kotlin@ultimum.io>


"""


import logging
from copy import deepcopy

import salt.utils.json
from salt.exceptions import SaltException
from salt.utils.versions import LooseVersion as _LooseVersion

log = logging.getLogger(__name__)


def __virtual__():
    """
    Only make these states available if Zabbix module is available.
    """
    if "zabbix.user_create" in __salt__:
        return True
    return (False, "zabbix module could not be loaded")


def admin_password_present(name, password=None, **kwargs):
    """
    Initial change of Zabbix Admin password to password taken from one of the sources (only the most prioritized one):
        1. 'password' parameter
        2. '_connection_password' parameter
        3. pillar 'zabbix.password' setting

    1) Tries to log in as Admin with password found in state password parameter or _connection_password
       or pillar or default zabbix password in this precise order, if any of them is present.
    2) If one of above passwords matches, it tries to change the password to the most prioritized one.
    3) If not able to connect with any password then it fails.

    :param name: Just a name of state
    :param password: Optional - desired password for Admin to be set
    :param _connection_user: Optional - Ignored in this state (always assumed 'Admin')
    :param _connection_password: Optional - zabbix password (can also be set in opts or pillar, see module's docstring)
    :param _connection_url: Optional - url of zabbix frontend (can also be set in opts, pillar, see module's docstring)

    .. code-block:: yaml

        # password taken from pillar or _connection_password
        zabbix-admin-password:
            zabbix_user.admin_password_present

        # directly set password
        zabbix-admin-password:
            zabbix_user.admin_password_present:
                - password: SECRET_PASS
    """
    login_error_messages = [
        "Login name or password is incorrect",
        "Incorrect user name or password",
    ]
    dry_run = __opts__["test"]
    default_zabbix_user = "Admin"
    default_zabbix_password = "zabbix"
    ret = {"name": name, "changes": {}, "result": False, "comment": ""}

    passwords = []
    connection_args = {}
    connection_args["_connection_user"] = default_zabbix_user
    if "_connection_url" in kwargs:
        connection_args["_connection_url"] = kwargs["_connection_url"]

    config_password = __salt__["config.option"]("zabbix.password", None)
    if config_password:
        passwords.append(config_password)
    if "_connection_password" in kwargs:
        passwords.append(kwargs["_connection_password"])
    if password:
        passwords.append(password)

    # get unique list in preserved order and reverse it
    seen = set()
    unique_passwords = [str(x) for x in passwords if x not in seen and not seen.add(x)]
    unique_passwords.reverse()

    if not unique_passwords:
        ret[
            "comment"
        ] = "Could not find any Zabbix Admin password setting! See documentation."
        return ret
    else:
        desired_password = unique_passwords[0]

    unique_passwords.append(default_zabbix_password)

    for pwd in unique_passwords:
        connection_args["_connection_password"] = pwd
        try:
            user_get = __salt__["zabbix.user_get"](
                default_zabbix_user, **connection_args
            )
        except SaltException as err:
            if all([x not in str(err) for x in login_error_messages]):
                raise
            user_get = False
        if user_get:
            if pwd == desired_password:
                ret["result"] = True
                ret["comment"] = "Admin password is correct."
                return ret
            else:
                break

    if user_get:
        if not dry_run:
            user_update = __salt__["zabbix.user_update"](
                user_get[0]["userid"], passwd=desired_password, **connection_args
            )
            if user_update:
                ret["result"] = True
                ret["changes"]["passwd"] = "changed to '" + str(desired_password) + "'"
        else:
            ret["result"] = None
            ret["comment"] = (
                "Password for user "
                + str(default_zabbix_user)
                + " updated to '"
                + str(desired_password)
                + "'"
            )

    return ret


def present(alias, passwd, usrgrps, medias=None, password_reset=False, **kwargs):
    """
    Ensures that the user exists, eventually creates new user.
    NOTE: use argument firstname instead of name to not mess values with name from salt sls.

    .. versionadded:: 2016.3.0

    :param alias: user alias
    :param passwd: user's password
    :param usrgrps: user groups to add the user to
    :param medias: Optional - user's medias to create
    :param password_reset: whether or not to reset password at update
    :param _connection_user: Optional - zabbix user (can also be set in opts or pillar, see module's docstring)
    :param _connection_password: Optional - zabbix password (can also be set in opts or pillar, see module's docstring)
    :param _connection_url: Optional - url of zabbix frontend (can also be set in opts, pillar, see module's docstring)
    :param firstname: string with firstname of the user, use 'firstname' instead of 'name' parameter to not mess \
    with value supplied from Salt sls file.

    .. code-block:: yaml

        make_user:
            zabbix_user.present:
                - alias: George
                - passwd: donottellanyonE@456x
                - password_reset: True
                - usrgrps:
                    - 13
                    - 7
                - medias:
                    - me@example.com:
                        - mediatype: mail
                        - period: '1-7,00:00-24:00'
                        - severity: NIWAHD
                    - make_jabber:
                        - active: true
                        - mediatype: jabber
                        - period: '1-5,08:00-19:00'
                        - sendto: jabbera@example.com
                    - text_me_morning_disabled:
                        - active: false
                        - mediatype: sms
                        - period: '1-5,09:30-10:00'
                        - severity: D
                        - sendto: '+42032132588568'

    """
    if medias is None:
        medias = []
    usrgrps = [int(x) for x in usrgrps]
    connection_args = {}
    if "_connection_user" in kwargs:
        connection_args["_connection_user"] = kwargs["_connection_user"]
    if "_connection_password" in kwargs:
        connection_args["_connection_password"] = kwargs["_connection_password"]
    if "_connection_url" in kwargs:
        connection_args["_connection_url"] = kwargs["_connection_url"]

    ret = {"name": alias, "changes": {}, "result": False, "comment": ""}

    # Comment and change messages
    comment_user_created = "User {} created.".format(alias)
    comment_user_updated = "User {} updated.".format(alias)
    comment_user_notcreated = "Unable to create user: {}. ".format(alias)
    comment_user_exists = "User {} already exists.".format(alias)
    changes_user_created = {
        alias: {
            "old": "User {} does not exist.".format(alias),
            "new": "User {} created.".format(alias),
        }
    }

    def _media_format(medias_data):
        """
        Formats medias from SLS file into valid JSON usable for zabbix API.
        Completes JSON with default values.

        :param medias_data: list of media data from SLS file

        """
        if not medias_data:
            return list()
        medias_json = salt.utils.json.loads(salt.utils.json.dumps(medias_data))
        medias_attr = ("active", "mediatype", "period", "severity", "sendto")
        media_type = {"mail": 1, "jabber": 2, "sms": 3}
        media_severities = ("D", "H", "A", "W", "I", "N")

        medias_dict = dict()
        for media in medias_json:
            for med in media:
                medias_dict[med] = dict()
                for medattr in media[med]:
                    for key, value in medattr.items():
                        if key in medias_attr:
                            medias_dict[med][key] = value

        medias_list = list()
        for key, value in medias_dict.items():
            # Load media values or default values
            active = "0" if str(value.get("active", "true")).lower() == "true" else "1"
            mediatype_sls = str(value.get("mediatype", "mail")).lower()
            mediatypeid = str(media_type.get(mediatype_sls, 1))
            period = value.get("period", "1-7,00:00-24:00")
            sendto = value.get("sendto", key)

            severity_sls = value.get("severity", "HD")
            severity_bin = ""
            for sev in media_severities:
                if sev in severity_sls:
                    severity_bin += "1"
                else:
                    severity_bin += "0"
            severity = str(int(severity_bin, 2))

            medias_list.append(
                {
                    "active": active,
                    "mediatypeid": mediatypeid,
                    "period": period,
                    "sendto": sendto,
                    "severity": severity,
                }
            )
        return medias_list

    user_exists = __salt__["zabbix.user_exists"](alias, **connection_args)
    zabbix_version = __salt__["zabbix.apiinfo_version"](**connection_args)

    if user_exists:
        user = __salt__["zabbix.user_get"](alias, **connection_args)[0]
        userid = user["userid"]

        update_usrgrps = False
        update_medias = False

        usergroups = deepcopy(user.get("usrgrps", []))
        cur_usrgrps = list()

        for usergroup in usergroups:
            cur_usrgrps.append(int(usergroup["usrgrpid"]))

        if set(cur_usrgrps) != set(usrgrps):
            update_usrgrps = True

        user_medias = user.get("medias", [])
        medias_formated = _media_format(medias)
        log.debug(user_medias)
        log.debug(medias_formated)

        if user_medias:
            user_medias_copy = deepcopy(user_medias)
            for user_med in user_medias_copy:
                user_med.pop("userid")
                user_med.pop("mediaid")
            media_diff = [x for x in medias_formated if x not in user_medias_copy] + [
                y for y in user_medias_copy if y not in medias_formated
            ]
            if media_diff:
                update_medias = True
        elif not user_medias and medias:
            update_medias = True

    # Dry run, test=true mode
    if __opts__["test"]:
        if user_exists:
            if update_usrgrps or password_reset or update_medias:
                ret["result"] = None
                ret["comment"] = comment_user_updated
            else:
                ret["result"] = True
                ret["comment"] = comment_user_exists
        else:
            ret["result"] = None
            ret["comment"] = comment_user_created

    error = []

    if user_exists:
        ret["result"] = True
        if update_usrgrps or password_reset or update_medias:
            ret["comment"] = comment_user_updated

            if _LooseVersion(zabbix_version) > _LooseVersion("3.4"):
                updates = deepcopy(connection_args)
                if update_usrgrps:
                    updates["usrgrps"] = usrgrps
                if password_reset:
                    updates["passwd"] = passwd
                if update_medias:
                    updates["medias"] = medias_formated
                result = __salt__["zabbix.user_update"](userid, **updates)

                new_user = __salt__["zabbix.user_get"](alias, **connection_args)[0]

                if "error" in result:
                    error.append(result["error"])
                else:
                    if update_usrgrps:
                        ret["changes"]["usrgrps"] = str(new_user["usrgrps"])
                    if update_medias:
                        ret["changes"]["medias"] = str(medias_formated)
                    if password_reset:
                        ret["changes"]["passwd"] = "updated"

                cur_usrgrps = list()
                for usergroup in new_user["usrgrps"]:
                    cur_usrgrps.append(int(usergroup["usrgrpid"]))

                usrgrp_diff = list(set(usrgrps) - set(cur_usrgrps))

                if usrgrp_diff and update_usrgrps:
                    error.append("Unable to update group(s): {}".format(usrgrp_diff))

            else:
                if update_usrgrps:
                    __salt__["zabbix.user_update"](
                        userid, usrgrps=usrgrps, **connection_args
                    )
                    updated_groups = __salt__["zabbix.usergroup_get"](
                        userids=userid, **connection_args
                    )

                    cur_usrgrps = list()
                    for usergroup in updated_groups:
                        cur_usrgrps.append(int(usergroup["usrgrpid"]))

                    usrgrp_diff = list(set(usrgrps) - set(cur_usrgrps))

                    if usrgrp_diff:
                        error.append(
                            "Unable to update group(s): {}".format(usrgrp_diff)
                        )

                    ret["changes"]["usrgrps"] = str(updated_groups)

                if password_reset:
                    updated_password = __salt__["zabbix.user_update"](
                        userid, passwd=passwd, **connection_args
                    )
                    if "error" in updated_password:
                        error.append(updated_groups["error"])
                    else:
                        ret["changes"]["passwd"] = "updated"

                if update_medias:
                    for user_med in user_medias:
                        deletedmed = __salt__["zabbix.user_deletemedia"](
                            user_med["mediaid"], **connection_args
                        )
                        if "error" in deletedmed:
                            error.append(deletedmed["error"])

                    for media in medias_formated:
                        updatemed = __salt__["zabbix.user_addmedia"](
                            userids=userid,
                            active=media["active"],
                            mediatypeid=media["mediatypeid"],
                            period=media["period"],
                            sendto=media["sendto"],
                            severity=media["severity"],
                            **connection_args
                        )

                        if "error" in updatemed:
                            error.append(updatemed["error"])

                    ret["changes"]["medias"] = str(medias_formated)

        else:
            ret["comment"] = comment_user_exists
    else:
        user_create = __salt__["zabbix.user_create"](alias, passwd, usrgrps, **kwargs)

        if "error" not in user_create:
            ret["result"] = True
            ret["comment"] = comment_user_created
            ret["changes"] = changes_user_created
        else:
            ret["result"] = False
            ret["comment"] = comment_user_notcreated + str(user_create["error"])

    # error detected
    if error:
        ret["changes"] = {}
        ret["result"] = False
        ret["comment"] = str(error)

    return ret


def absent(name, **kwargs):
    """
    Ensures that the user does not exist, eventually delete user.

    .. versionadded:: 2016.3.0

    :param name: user alias
    :param _connection_user: Optional - zabbix user (can also be set in opts or pillar, see module's docstring)
    :param _connection_password: Optional - zabbix password (can also be set in opts or pillar, see module's docstring)
    :param _connection_url: Optional - url of zabbix frontend (can also be set in opts, pillar, see module's docstring)

    .. code-block:: yaml

        George:
            zabbix_user.absent

    """
    connection_args = {}
    if "_connection_user" in kwargs:
        connection_args["_connection_user"] = kwargs["_connection_user"]
    if "_connection_password" in kwargs:
        connection_args["_connection_password"] = kwargs["_connection_password"]
    if "_connection_url" in kwargs:
        connection_args["_connection_url"] = kwargs["_connection_url"]

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

    # Comment and change messages
    comment_user_deleted = "USer {} deleted.".format(name)
    comment_user_notdeleted = "Unable to delete user: {}. ".format(name)
    comment_user_notexists = "User {} does not exist.".format(name)
    changes_user_deleted = {
        name: {
            "old": "User {} exists.".format(name),
            "new": "User {} deleted.".format(name),
        }
    }

    user_get = __salt__["zabbix.user_get"](name, **connection_args)

    # Dry run, test=true mode
    if __opts__["test"]:
        if not user_get:
            ret["result"] = True
            ret["comment"] = comment_user_notexists
        else:
            ret["result"] = None
            ret["comment"] = comment_user_deleted
            ret["changes"] = changes_user_deleted

    if not user_get:
        ret["result"] = True
        ret["comment"] = comment_user_notexists
    else:
        try:
            userid = user_get[0]["userid"]
            user_delete = __salt__["zabbix.user_delete"](userid, **connection_args)
        except KeyError:
            user_delete = False

        if user_delete and "error" not in user_delete:
            ret["result"] = True
            ret["comment"] = comment_user_deleted
            ret["changes"] = changes_user_deleted
        else:
            ret["result"] = False
            ret["comment"] = comment_user_notdeleted + str(user_delete["error"])

    return ret
