"""
Control of entries in SSH authorized_key files
==============================================

The information stored in a user's SSH authorized key file can be easily
controlled via the ssh_auth state. Defaults can be set by the enc, options,
and comment keys. These defaults can be overridden by including them in the
name.

Since the YAML specification limits the length of simple keys to 1024
characters, and since SSH keys are often longer than that, you may have
to use a YAML 'explicit key', as demonstrated in the second example below.

.. code-block:: yaml

    AAAAB3NzaC1kc3MAAACBAL0sQ9fJ5bYTEyY==:
      ssh_auth.present:
        - user: root
        - enc: ssh-dss

    ? AAAAB3NzaC1kc3MAAACBAL0sQ9fJ5bYTEyY==...
    :
      ssh_auth.present:
        - user: root
        - enc: ssh-dss

    thatch:
      ssh_auth.present:
        - user: root
        - source: salt://ssh_keys/thatch.id_rsa.pub
        - config: '%h/.ssh/authorized_keys'

    sshkeys:
      ssh_auth.present:
        - user: root
        - enc: ssh-rsa
        - options:
          - option1="value1"
          - option2="value2 flag2"
        - comment: myuser
        - names:
          - AAAAB3NzaC1kc3MAAACBAL0sQ9fJ5bYTEyY==
          - ssh-dss AAAAB3NzaCL0sQ9fJ5bYTEyY== user@domain
          - option3="value3" ssh-dss AAAAB3NzaC1kcQ9J5bYTEyY== other@testdomain
          - AAAAB3NzaC1kcQ9fJFF435bYTEyY== newcomment

    sshkeys:
      ssh_auth.manage:
        - user: root
        - enc: ssh-rsa
        - options:
          - option1="value1"
          - option2="value2 flag2"
        - comment: myuser
        - ssh_keys:
          - AAAAB3NzaC1kc3MAAACBAL0sQ9fJ5bYTEyY==
          - ssh-dss AAAAB3NzaCL0sQ9fJ5bYTEyY== user@domain
          - option3="value3" ssh-dss AAAAB3NzaC1kcQ9J5bYTEyY== other@testdomain
          - AAAAB3NzaC1kcQ9fJFF435bYTEyY== newcomment
"""

import re
import sys


def _present_test(
    user, name, enc, comment, options, source, config, fingerprint_hash_type
):
    """
    Run checks for "present"
    """
    result = None
    if source:
        keys = __salt__["ssh.check_key_file"](
            user,
            source,
            config,
            saltenv=__env__,
            fingerprint_hash_type=fingerprint_hash_type,
        )
        if keys:
            comment = ""
            for key, status in keys.items():
                if status == "exists":
                    continue
                comment += f"Set to {status}: {key}\n"
            if comment:
                return result, comment
        err = sys.modules[__salt__["test.ping"].__module__].__context__.pop(
            "ssh_auth.error", None
        )
        if err:
            return False, err
        else:
            return (
                True,
                f"All host keys in file {source} are already present",
            )
    else:
        # check if this is of form {options} {enc} {key} {comment}
        sshre = re.compile(r"^(.*?)\s?((?:sk-)?(?:ssh\-|ecds)[\w@.-]+\s.+)$")
        fullkey = sshre.search(name)
        # if it is {key} [comment]
        if not fullkey:
            key_and_comment = name.split()
            name = key_and_comment[0]
            if len(key_and_comment) == 2:
                comment = key_and_comment[1]
        else:
            # if there are options, set them
            if fullkey.group(1):
                options = fullkey.group(1).split(",")
            # key is of format: {enc} {key} [comment]
            comps = fullkey.group(2).split()
            enc = comps[0]
            name = comps[1]
            if len(comps) == 3:
                comment = comps[2]

    check = __salt__["ssh.check_key"](
        user,
        name,
        enc,
        comment,
        options,
        config=config,
        fingerprint_hash_type=fingerprint_hash_type,
    )
    if check == "update":
        comment = f"Key {name} for user {user} is set to be updated"
    elif check == "add":
        comment = f"Key {name} for user {user} is set to be added"
    elif check == "exists":
        result = True
        comment = "The authorized host key {} is already present for user {}".format(
            name, user
        )

    return result, comment


def _absent_test(
    user, name, enc, comment, options, source, config, fingerprint_hash_type
):
    """
    Run checks for "absent"
    """
    result = None
    if source:
        keys = __salt__["ssh.check_key_file"](
            user,
            source,
            config,
            saltenv=__env__,
            fingerprint_hash_type=fingerprint_hash_type,
        )
        if keys:
            comment = ""
            for key, status in list(keys.items()):
                if status == "add":
                    continue
                comment += f"Set to remove: {key}\n"
            if comment:
                return result, comment
        err = sys.modules[__salt__["test.ping"].__module__].__context__.pop(
            "ssh_auth.error", None
        )
        if err:
            return False, err
        else:
            return (True, f"All host keys in file {source} are already absent")
    else:
        # check if this is of form {options} {enc} {key} {comment}
        sshre = re.compile(r"^(.*?)\s?((?:sk-)?(?:ssh\-|ecds)[\w@.-]+\s.+)$")
        fullkey = sshre.search(name)
        # if it is {key} [comment]
        if not fullkey:
            key_and_comment = name.split()
            name = key_and_comment[0]
            if len(key_and_comment) == 2:
                comment = key_and_comment[1]
        else:
            # if there are options, set them
            if fullkey.group(1):
                options = fullkey.group(1).split(",")
            # key is of format: {enc} {key} [comment]
            comps = fullkey.group(2).split()
            enc = comps[0]
            name = comps[1]
            if len(comps) == 3:
                comment = comps[2]

    check = __salt__["ssh.check_key"](
        user,
        name,
        enc,
        comment,
        options,
        config=config,
        fingerprint_hash_type=fingerprint_hash_type,
    )
    if check == "update" or check == "exists":
        comment = f"Key {name} for user {user} is set for removal"
    else:
        comment = "Key is already absent"
        result = True

    return result, comment


def present(
    name,
    user,
    enc="ssh-rsa",
    comment="",
    source="",
    options=None,
    config=".ssh/authorized_keys",
    fingerprint_hash_type=None,
    **kwargs,
):
    """
    Verifies that the specified SSH key is present for the specified user

    name
        The SSH key to manage

    user
        The user who owns the SSH authorized keys file to modify

    enc
        Defines what type of key is being used, can be ed25519, ecdsa,
        ssh-rsa, ssh-dss or any other type as of openssh server version 8.7.

    comment
        The comment to be placed with the SSH public key

    source
        The source file for the key(s). Can contain any number of public keys,
        in standard "authorized_keys" format. If this is set, comment and enc
        will be ignored.

    .. note::
        The source file must contain keys in the format ``<enc> <key>
        <comment>``. If you have generated a keypair using PuTTYgen, then you
        will need to do the following to retrieve an OpenSSH-compatible public
        key.

        1. In PuTTYgen, click ``Load``, and select the *private* key file (not
           the public key), and click ``Open``.
        2. Copy the public key from the box labeled ``Public key for pasting
           into OpenSSH authorized_keys file``.
        3. Paste it into a new file.

    options
        The options passed to the key, pass a list object.
        If set, this will overwrite the ``options`` to all keys in source file

    config
        The location of the authorized keys file relative to the user's home
        directory, defaults to ".ssh/authorized_keys". Token expansion %u and
        %h for username and home path supported.

    fingerprint_hash_type
        The public key fingerprint hash type that the public key fingerprint
        was originally hashed with. This defaults to ``sha256`` if not specified.
    """
    ret = {"name": name, "changes": {}, "result": True, "comment": ""}

    if source == "":
        # check if this is of form {options} {enc} {key} {comment}
        sshre = re.compile(r"^(.*?)\s?((?:sk-)?(?:ssh\-|ecds)[\w@.-]+\s.+)$")
        fullkey = sshre.search(name)
        # if it is {key} [comment]
        if not fullkey:
            key_and_comment = name.split(None, 1)
            name = key_and_comment[0]
            if len(key_and_comment) == 2:
                comment = key_and_comment[1]
        else:
            # if there are options, set them
            if fullkey.group(1):
                options = fullkey.group(1).split(",")
            # key is of format: {enc} {key} [comment]
            comps = fullkey.group(2).split(None, 2)
            enc = comps[0]
            name = comps[1]
            if len(comps) == 3:
                comment = comps[2]

    if __opts__["test"]:
        ret["result"], ret["comment"] = _present_test(
            user,
            name,
            enc,
            comment,
            options or [],
            source,
            config,
            fingerprint_hash_type,
        )
        return ret

    if source != "":
        # Get only the path to the file without env references to check if exists
        source_path = __salt__["cp.get_url"](source, None, saltenv=__env__)

        # Extract Key from file if source is present
        if not source_path:
            data = "no key"
        else:
            # ssh.set_auth_key_from_file already reads and add/replace all keys
            # from source file.
            data = __salt__["ssh.set_auth_key_from_file"](
                user,
                source,
                config=config,
                saltenv=__env__,
                fingerprint_hash_type=fingerprint_hash_type,
                options=options,
            )
    else:
        data = __salt__["ssh.set_auth_key"](
            user,
            name,
            enc=enc,
            comment=comment,
            options=options or [],
            config=config,
            fingerprint_hash_type=fingerprint_hash_type,
        )

    if data == "replace":
        ret["changes"][name] = "Updated"
        ret["comment"] = "The authorized host key {} for user {} was updated".format(
            name, user
        )
        return ret
    elif data == "no change":
        ret["comment"] = (
            "The authorized host key {} is already present for user {}".format(
                name, user
            )
        )
    elif data == "new":
        ret["changes"][name] = "New"
        ret["comment"] = "The authorized host key {} for user {} was added".format(
            name, user
        )
    elif data == "no key":
        ret["result"] = False
        ret["comment"] = "Failed to add the ssh key. Source file {} is missing".format(
            source
        )
    elif data == "fail":
        ret["result"] = False
        err = sys.modules[__salt__["test.ping"].__module__].__context__.pop(
            "ssh_auth.error", None
        )
        if err:
            ret["comment"] = err
        else:
            ret["comment"] = (
                "Failed to add the ssh key. Is the home "
                "directory available, and/or does the key file "
                "exist?"
            )
    elif data == "invalid" or data == "Invalid public key":
        ret["result"] = False
        ret["comment"] = (
            "Invalid public ssh key, most likely has spaces or invalid syntax"
        )

    return ret


def absent(
    name,
    user,
    enc="ssh-rsa",
    comment="",
    source="",
    options=None,
    config=".ssh/authorized_keys",
    fingerprint_hash_type=None,
):
    """
    Verifies that the specified SSH key is absent

    name
        The SSH key to manage

    user
        The user who owns the SSH authorized keys file to modify

    enc
        Defines what type of key is being used, can be ed25519, ecdsa,
        ssh-rsa, ssh-dss or any other type as of openssh server version 8.7.

    comment
        The comment to be placed with the SSH public key

    options
        The options passed to the key, pass a list object

    source
        The source file for the key(s). Can contain any number of public keys,
        in standard "authorized_keys" format. If this is set, comment, enc and
        options will be ignored.

        .. versionadded:: 2015.8.0

    config
        The location of the authorized keys file relative to the user's home
        directory, defaults to ".ssh/authorized_keys". Token expansion %u and
        %h for username and home path supported.

    fingerprint_hash_type
        The public key fingerprint hash type that the public key fingerprint
        was originally hashed with. This defaults to ``sha256`` if not specified.

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

    if __opts__["test"]:
        ret["result"], ret["comment"] = _absent_test(
            user,
            name,
            enc,
            comment,
            options or [],
            source,
            config,
            fingerprint_hash_type,
        )
        return ret

    if source != "":
        # Get only the path to the file without env references to check if exists
        source_path = __salt__["cp.get_url"](source, None, saltenv=__env__)

        # Extract Key from file if source is present
        if not source_path:
            data = "no key"
        else:
            # ssh.rm_auth_key_from_file already reads and delete all keys
            # from source file.
            ret["comment"] = __salt__["ssh.rm_auth_key_from_file"](
                user,
                source,
                config,
                saltenv=__env__,
                fingerprint_hash_type=fingerprint_hash_type,
            )
    else:
        # Get just the key
        sshre = re.compile(r"^(.*?)\s?((?:sk-)?(?:ssh\-|ecds)[\w@.-]+\s.+)$")
        fullkey = sshre.search(name)
        # if it is {key} [comment]
        if not fullkey:
            key_and_comment = name.split(None, 1)
            name = key_and_comment[0]
            if len(key_and_comment) == 2:
                comment = key_and_comment[1]
        else:
            # if there are options, set them
            if fullkey.group(1):
                options = fullkey.group(1).split(",")
            # key is of format: {enc} {key} [comment]
            comps = fullkey.group(2).split()
            enc = comps[0]
            name = comps[1]
            if len(comps) == 3:
                comment = comps[2]
        ret["comment"] = __salt__["ssh.rm_auth_key"](
            user, name, config=config, fingerprint_hash_type=fingerprint_hash_type
        )

    if ret["comment"] == "User authorized keys file not present":
        ret["result"] = False
        return ret
    elif ret["comment"] == "Key removed":
        ret["changes"][name] = "Removed"

    return ret


def manage(
    name,
    ssh_keys,
    user,
    enc="ssh-rsa",
    comment="",
    source="",
    options=None,
    config=".ssh/authorized_keys",
    fingerprint_hash_type=None,
    **kwargs,
):
    """
    .. versionadded:: 3000

    Ensures that only the specified ssh_keys are present for the specified user

    ssh_keys
        The SSH key to manage

    user
        The user who owns the SSH authorized keys file to modify

    enc
        Defines what type of key is being used, can be ed25519, ecdsa,
        ssh-rsa, ssh-dss or any other type as of openssh server version 8.7.

    comment
        The comment to be placed with the SSH public key

    source
        The source file for the key(s). Can contain any number of public keys,
        in standard "authorized_keys" format. If this is set, comment and enc
        will be ignored.

    .. note::
        The source file must contain keys in the format ``<enc> <key>
        <comment>``. If you have generated a keypair using PuTTYgen, then you
        will need to do the following to retrieve an OpenSSH-compatible public
        key.

        1. In PuTTYgen, click ``Load``, and select the *private* key file (not
           the public key), and click ``Open``.
        2. Copy the public key from the box labeled ``Public key for pasting
           into OpenSSH authorized_keys file``.
        3. Paste it into a new file.

    options
        The options passed to the keys, pass a list object

    config
        The location of the authorized keys file relative to the user's home
        directory, defaults to ".ssh/authorized_keys". Token expansion %u and
        %h for username and home path supported.

    fingerprint_hash_type
        The public key fingerprint hash type that the public key fingerprint
        was originally hashed with. This defaults to ``sha256`` if not specified.
    """
    ret = {"name": "", "changes": {}, "result": True, "comment": ""}

    all_potential_keys = []
    for ssh_key in ssh_keys:
        # gather list potential ssh keys for removal comparison
        # options, enc, and comments could be in the mix
        all_potential_keys.extend(ssh_key.split(" "))

    existing_keys = __salt__["ssh.auth_keys"](
        user=user, config=config, fingerprint_hash_type=fingerprint_hash_type
    ).keys()

    remove_keys = set(existing_keys).difference(all_potential_keys)
    for remove_key in remove_keys:
        if __opts__["test"]:
            remove_comment = f"{remove_key} Key set for removal"
            ret["comment"] = remove_comment
            ret["result"] = None
        else:
            remove_comment = absent(
                remove_key,
                user,
                enc=enc,
                comment=comment,
                source=source,
                options=options,
                config=config,
                fingerprint_hash_type=fingerprint_hash_type,
                **kwargs,
            )["comment"]
            ret["changes"][remove_key] = remove_comment

    for ssh_key in ssh_keys:
        run_return = present(
            ssh_key,
            user,
            enc,
            comment,
            source,
            options,
            config,
            fingerprint_hash_type,
            **kwargs,
        )
        if run_return["changes"]:
            ret["changes"].update(run_return["changes"])
        else:
            ret["comment"] += "\n" + run_return["comment"]
            ret["comment"] = ret["comment"].strip()

        if run_return["result"] is None:
            ret["result"] = None
        elif not run_return["result"]:
            ret["result"] = False

    return ret
