import socket
from base64 import b64decode, b64encode
from json import loads, dumps
from logging import getLogger
from os.path import join
from urllib import request
from urllib.parse import urlencode

from consul_kv.settings import DEFAULT_KV_ENDPOINT, DEFAULT_TXN_ENDPOINT

log = getLogger(__name__)


def put_kv(
    k, v, cas=None, endpoint=DEFAULT_KV_ENDPOINT, timeout=socket._GLOBAL_DEFAULT_TIMEOUT
):
    """
    Put a key and value to the distributed key value store at the location path
    :param str k: the key to put
    :param int cas: the cas version number. If None, no cas is used.
    :param str v: the value to put
    :param str endpoint: API url to PUT to
    :param int timeout: Seconds before timing out
    :return None:
    """
    encoded = str.encode(str(v))
    params = dict()
    if cas:
        params["cas"] = cas

    url = join(endpoint, k) if k else endpoint

    if params:
        url = "{}/?{}".format(url, urlencode(params))

    req = request.Request(url=url, data=encoded, method="PUT")
    with request.urlopen(req, timeout=timeout) as f:
        log.debug(
            "PUT k v pair ({}, {}) to {}: {}, {}".format(k, v, url, f.status, f.reason)
        )


def _mapping_to_txn_data(mapping, verb="set"):
    """
    Transform a key value mapping to a list of operations to perform
    inside the atomic transaction.
    :param dict mapping: flat dict of key/values, or key/(value, modification-index, verb)
    :param str verb: The type of operation to perform. See the list of possibilities
    here https://www.consul.io/docs/agent/http/kv.html#txn
    :return list[dict, ..] txn_data: List of dicts describing the operations to perform
    """
    txn_data = [
        {
            "KV": {
                "Verb": verb,
                "Key": k,
                "Value": b64encode(str(v).encode("utf-8")).decode("utf-8"),
            }
        }
        for k, v in mapping.items()
    ]
    if verb == "cas":
        # If the index is 0, Consul will only put the key if it does not already exist.
        # See https://www.consul.io/api/kv.html#create-update-key
        for item in txn_data:
            item["Index"] = 0
    return txn_data


def put_kv_txn(
    mapping,
    endpoint=DEFAULT_TXN_ENDPOINT,
    verb="set",
    timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
):
    """
    Update multiple keys inside a single, atomic transaction.
    The body of the request should be a list of operations to
    perform inside the atomic transaction. Up to 64 operations
    may be present in a single transaction. Each Base64-encoded
    blob of data can not be larger than 512kB.
    https://www.consul.io/docs/agent/http/kv.html
    :param dict mapping: flat dict of key/values put
    :param str endpoint: API url to PUT to. Should be a txn endpoint.
    :param str verb: The type of operation to perform. See the list of possibilities
    here https://www.consul.io/docs/agent/http/kv.html#txn
    :param int timeout: Seconds before timing out
    :return None:
    """
    txn_data = _mapping_to_txn_data(mapping, verb=verb)
    data = dumps(txn_data).encode("utf-8")
    req = request.Request(
        url=endpoint,
        data=data,
        method="PUT",
        headers={"Content-Type": "application/json"},
    )
    with request.urlopen(req, timeout=timeout) as f:
        log.debug(
            "PUT k v mapping {} to {}: {}, {}".format(
                mapping, endpoint, f.status, f.reason
            )
        )


def get_kv_builder(postprocessor=lambda x: x):
    """
    Function that returns a function that gets the key value mapping
    from the distributed key value store and postprocesses the result
    before returning the value.
    :param func postprocessor: The function to apply to the result
    before returning it as the result of the returned function
    The default postprocessor simply returns the value as is.
    :return func get_kv_raw: The function that gets the key value
    and applies the passed postprocessor before returning the retrieved
    value
    """

    def get_kv_raw(
        k=None,
        recurse=False,
        endpoint=DEFAULT_KV_ENDPOINT,
        timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
        decode_utf8=True,
    ):
        """
        Get the key value mapping from the distributed key value store and
        apply the defined postprocessor from the outer context before
        returning the retrieved value.
        :param str k: key to get
        :param bool recurse: whether or not to recurse over the path and
        retrieve all nested values
        :param str endpoint: API url to get the value from
        :param int timeout: Seconds before timing out
        :param bool decode_utf8: Whether or not to decode the values as
        utf-8 text. This is True by default. Pass False here if you wish
        to retrieve raw blobs of data.
        :return dict|mul mapping: key value mapping, or the return value
        as defined in the specified postprocessor of the outer function.
        """
        url = join(endpoint, k) if k else endpoint
        url = "{}/?recurse".format(url) if recurse else url
        req = request.Request(url=url, method="GET")
        with request.urlopen(req, timeout=timeout) as r:
            result = loads(r.read().decode("utf-8"))
        return postprocessor(result, decode_utf8=decode_utf8)

    return get_kv_raw


get_kv_meta = get_kv_builder()

get_kv_cas = get_kv_builder(
    lambda result, decode_utf8: {
        r["Key"]: (
            (
                (
                    b64decode(r["Value"]).decode("utf-8")
                    if decode_utf8
                    else b64decode(r["Value"])
                ),
                r["ModifyIndex"],
            )
            if r["Value"]
            else None
        )
        for r in result
        if r["Key"]
    }
)

get_kv = get_kv_builder(
    lambda result, decode_utf8: {
        # values are stored base64 encoded in consul, they
        # are decoded before returned by this function.
        r["Key"]: (
            (
                b64decode(r["Value"]).decode("utf-8")
                if decode_utf8
                else b64decode(r["Value"])
            )
            if r["Value"]
            else None
        )
        for r in result
        if r["Key"]
    }
)


def delete_kv(
    k=None,
    cas=None,
    recurse=False,
    endpoint=DEFAULT_KV_ENDPOINT,
    timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
):
    """
    Delete a key from the distributed key value store
    :param str k: the key to delete
    :param int cas: the cas version number. If None, no cas is used.
    :param bool recurse: recurse the path and delete all entries
    :param str endpoint: API url to DELETE
    :param int timeout: Seconds before timing out
    :return:
    """
    params = dict()
    if cas:
        params["cas"] = cas
    if recurse:
        params["recurse"] = recurse

    url = join(endpoint, k) if k else endpoint

    if params:
        url = "{}/?{}".format(url, urlencode(params))

    req = request.Request(url=url, method="DELETE")
    with request.urlopen(req, timeout=timeout) as f:
        log.debug(
            "DELETEd key {}{}: {} {}".format(
                url, " recursively" if recurse else "", f.status, f.reason
            )
        )
