"""
Grains from cloud metadata servers at 169.254.169.254

.. versionadded:: 2017.7.0

:depends: requests

To enable these grains that pull from the http://169.254.169.254/latest
metadata server set `metadata_server_grains: True` in the minion config.

.. code-block:: yaml

    metadata_server_grains: True

"""

import os
import socket

import salt.utils.data
import salt.utils.http as http
import salt.utils.json
import salt.utils.stringutils

# metadata server information
IP = "169.254.169.254"
HOST = f"http://{IP}/"


def __virtual__():
    if __opts__.get("metadata_server_grains", False) is False:
        return False
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(0.1)
    result = sock.connect_ex((IP, 80))
    if result != 0:
        return False
    if http.query(os.path.join(HOST, "latest/"), status=True).get("status") != 200:
        # Initial connection failed, might need a token
        _refresh_token()
        if (
            http.query(
                os.path.join(HOST, "latest/"),
                status=True,
                header_dict={
                    "X-aws-ec2-metadata-token": __context__["metadata_aws_token"]
                },
            ).get("status")
            != 200
        ):
            return False
    return True


def _refresh_token():
    __context__["metadata_aws_token"] = http.query(
        os.path.join(HOST, "latest/api/token"),
        method="PUT",
        header_dict={"X-aws-ec2-metadata-token-ttl-seconds": "21600"},
    ).get("body")


def _search(prefix="latest/"):
    """
    Recursively look up all grains in the metadata server
    """
    ret = {}
    if "metadata_aws_token" in __context__:
        if (
            http.query(
                os.path.join(HOST, "latest/"),
                status=True,
                header_dict={
                    "X-aws-ec2-metadata-token": __context__["metadata_aws_token"]
                },
            ).get("status")
            != 200
        ):
            _refresh_token()

        linedata = http.query(
            os.path.join(HOST, prefix),
            header_dict={"X-aws-ec2-metadata-token": __context__["metadata_aws_token"]},
            headers=True,
        )
    else:
        linedata = http.query(os.path.join(HOST, prefix), headers=True)
    if "body" not in linedata:
        return ret
    body = salt.utils.stringutils.to_unicode(linedata["body"])
    # Since 3006.3, salt.utils.http.query (tornado backend) returns ``body``
    # on HTTPError but does not populate ``headers``. Treat a missing
    # ``headers`` key as "no Content-Type information" rather than letting
    # KeyError propagate and break the whole grain load (#65184).
    response_headers = linedata.get("headers") or {}
    if response_headers.get("Content-Type", "text/plain") == "application/octet-stream":
        return body
    for line in body.split("\n"):
        if line.endswith("/"):
            ret[line[:-1]] = _search(prefix=os.path.join(prefix, line))
        elif line == "user-data":
            # user-data is returned verbatim; do not fall through to the
            # "=" splitter, which would corrupt user-data containing "="
            # characters (e.g. cloud-init #cloud-config payloads).
            retdata = http.query(os.path.join(HOST, prefix, line)).get("body", None)
            ret[line] = retdata
        elif prefix == "latest/":
            # (gtmanfred) The first level should have a forward slash since
            # they have stuff underneath. This will not be doubled up though,
            # because lines ending with a slash are checked first.
            ret[line] = _search(prefix=os.path.join(prefix, line + "/"))
        elif line.endswith(("dynamic", "meta-data")):
            ret[line] = _search(prefix=os.path.join(prefix, line))
        elif "=" in line:
            key, value = line.split("=")
            ret[value] = _search(prefix=os.path.join(prefix, key))
        else:
            if "metadata_aws_token" in __context__:
                retdata = http.query(
                    os.path.join(HOST, prefix, line),
                    header_dict={
                        "X-aws-ec2-metadata-token": __context__["metadata_aws_token"]
                    },
                ).get("body", None)
            else:
                retdata = http.query(os.path.join(HOST, prefix, line)).get("body", None)
            # (gtmanfred) This try except block is slightly faster than
            # checking if the string starts with a curly brace
            if isinstance(retdata, bytes):
                try:
                    ret[line] = salt.utils.json.loads(
                        salt.utils.stringutils.to_unicode(retdata)
                    )
                except ValueError:
                    ret[line] = salt.utils.stringutils.to_unicode(retdata)
            else:
                ret[line] = retdata
    return salt.utils.data.decode(ret)


def metadata():
    return _search()
