import base64
import hashlib
import logging
import os
import threading
import time
import uuid
from collections import namedtuple

from kazoo import security
from kazoo.client import KazooClient
from kazoo.exceptions import NoNodeError
from kazoo.exceptions import KazooException
from k3confloader import conf
import k3net
import k3utfjson

from .exceptions import ZKWaitTimeout

logger = logging.getLogger(__name__)

PERM_TO_LONG = {
    "c": "create",
    "d": "delete",
    "r": "read",
    "w": "write",
    "a": "admin",
}

PERM_TO_SHORT = {
    "create": "c",
    "delete": "d",
    "read": "r",
    "write": "w",
    "admin": "a",
}

# We assumes that ip does not change during process running.
# Display intra ip if presents, or display pub ip.
host_ip4 = k3net.ips_prefer(k3net.get_host_ip4(), k3net.INN)


class PermTypeError(Exception):
    pass


class ZkPathError(Exception):
    pass


def close_zk(zk):
    """
    Stop and close a zk client.
    :param zk: a `KazooClient` or `KazooClientExt` instance, otherwise raise a `TypeError`.
    :return: nothing
    """
    if not isinstance(zk, KazooClient):
        raise TypeError("expect KazooClient or KazooClientExt, but got {t}".format(t=type(zk)))

    try:
        zk.stop()

    except KazooException as e:
        logger.exception(repr(e) + " while stop zk client")

    try:
        zk.close()

    except Exception as e:
        logger.exception(repr(e) + " while close zk client")


def lock_data(node_id=None):
    # deprecated
    return lock_id(node_id=node_id)


def lock_id(node_id=None):
    """
    It builds a string used as zk lock node value, to describe lock holder's information.
    The result string is in form:
    <node_id>-<ip>-<process_id>-<counter>
    # e.g. web-192.168.0.2-1233-0000000001

    `ip` is chosen from all local ipv4.
    If there is an intra net ip, use it.
    Otherwise, use an public ip it found.
    :param node_id: is an arbitrary string representing a host.
    If it is `None`, `config.zk_node_id` is used.

    :return: a string for lock identifier.
    """
    """
    Embed lock holder information into the zk node data for the lock.

    `node_id` is a user defined identifier of a host.
    """

    if node_id is None:
        node_id = conf.zk_node_id

    ip = (host_ip4 + ["unknownip"])[0]

    seq = [node_id, ip, str(os.getpid()), str(uuid.uuid4()).replace("-", "")]

    return "-".join(seq)


def parse_lock_data(data_str):
    # deprecated
    return parse_lock_id(data_str)


def parse_lock_id(data_str):
    """
    It parse string built by `zkutil.lock_id()` to an dictionary.
    :param data_str: string built by `zkutil.lock_id()`.
    """
    # :return:
    # {
    #     "node_id": "<node_id>",
    #     "ip": "<ip>",
    #     "process_id": process_id,
    #     "counter": <integer>,
    # "txid": "<txid>",
    # }
    """
    Parse string generated by lock_id()
    `process_id` and `counter` is `int`, the others are `string`s.

    If any of these three field can not be parsed correctly, it will be `None`.
    If `node_id` is in form of `txid:...`, `txid` is filled with the text after `:`.
    Otherwise it is `None`.
    This is added for zk-transaction implementation.
    """

    node_id, ip, process_id, _uuid = (data_str.split("-", 3) + ([None] * 4))[:4]

    if isinstance(process_id, str) and process_id.isdigit():
        process_id = int(process_id)
    else:
        process_id = None

    rst = {"node_id": node_id, "ip": ip, "process_id": process_id, "uuid": _uuid, "txid": None}

    if node_id.startswith("txid:"):
        rst["txid"] = node_id.split(":", 1)[1]

    return rst


def make_digest(acc):
    # acc = "username:password"
    """
    It make a digest for string acc

    :param acc: The `acc` string is in form:
    :return: a digest string.
    """
    digest = hashlib.sha1(acc.encode()).digest()
    rst = base64.b64encode(digest).strip().decode()
    return rst


def make_acl_entry(username, password, permissions):
    """
    It concat `username`, `digest` and `permissions` to a string as acl entry.
    :param username: the name of zookeeper user.
    :param password: the password of zookeeper user.
    :param permissions: a string, a list or a tuple.
    each element in `permissions` is a char who should be included in `cdrwa`. If `permissions` contains extra element, `PermTypeError` will be raised.
    :return: a string in form:
    "digest:<username>:<digest>:<permissions>"
    where `digest` is a string build by `zkutil.make_digest()`
    """
    perms = ""
    for c in permissions:
        if c not in PERM_TO_LONG:
            raise PermTypeError(c)
        perms += c

    return "digest:{username}:{digest}:{permissions}".format(
        username=username, digest=make_digest(username + ":" + password), permissions=perms
    )


def perm_to_long(short, lower=True):
    """
    Convert short style zookeeper permissions(`cdrwa` or `CDRWA`)
    to standard permission list(`['create', 'delete', 'read', 'write', 'admin']`).
    :param short: is `iterable` of short permissions that can be used in `for-loop`.
    Such as `"cdrw"` or `['c', 'd']`
    :param lower: specifies if convert result to lower or upper case.
    By default it is `True`, for lower case.
    :return: a list of standard permission.
    """
    rst = []

    for c in short:
        c = c.lower()
        if c not in PERM_TO_LONG:
            raise PermTypeError(c)

        rst.append(PERM_TO_LONG[c])

    if not lower:
        rst = [x.upper() for x in rst]

    return rst


def perm_to_short(lst, lower=True):
    """
    The reverse of `perm_to_long`:
    It convert a list of standard permissions back to a short permission string,
    such as `cdrw`.
    :param lst: is a list of standard permissions, such as `['create', 'read']`.
    :param lower: specifies if convert result to lower or upper case.
    By default it is `True`, for lower case.
    :return: a string of short permissions.
    """
    rst = ""

    for p in lst:
        p = p.lower()
        if p not in PERM_TO_SHORT:
            raise PermTypeError(p)

        rst += PERM_TO_SHORT[p]

    if not lower:
        rst = rst.upper()

    return rst


def make_kazoo_digest_acl(acl):
    # acl = (('xp', '123', 'cdrwa'),
    #        ('foo', 'passw', 'rw'),
    # )
    # print zkutil.make_kazoo_digest_acl(acl)
    #
    # [
    #     ACL(perms=12, acl_list=['CREATE', 'DELETE'],
    #         id=Id(scheme='digest', id=u'foo:VNy+Z9IdXrOUk9Rtia4fQS071t4=')),
    #     ACL(perms=31, acl_list=['ALL'],
    #         id=Id(scheme='digest', id=u'xp:LNcZO17uqqJ4GuYoSclIsGjYniQ='))
    # ]

    """

    :param acl: acl in tuple or list.
    :return: a `list` of `kazoo.security.ACL`.
    """
    if acl is None:
        return None

    rst = []
    for name, passw, perms in acl:
        perm_dict = {p: True for p in perm_to_long(perms)}
        acl_entry = security.make_digest_acl(name, passw, **perm_dict)
        rst.append(acl_entry)

    return rst


def parse_kazoo_acl(acls):
    """
    Convert kazoo style acls in list/tuple to a list in form `[(<scheme>, <user>, <perm>)]`.
    :param acls: a list/tuple of `kazoo.security.ACL`.
    :return: a list of acl in form `[(<scheme>, <user>, <perm>)]`
    """
    # acls = [ACL(perms=31,
    #            acl_list=['ALL'],
    #            id=Id(scheme='digest', id=u'user:+Ir5sN1lGJEEs8xBZhZXK='))]

    rst = []
    for acl in acls:
        if "ALL" in acl.acl_list:
            acl_list = "cdrwa"
        else:
            acl_list = perm_to_short(acl.acl_list)

        rst.append((acl.id.scheme, acl.id.id.split(":")[0], acl_list))

    return rst


def is_backward_locking(locked_keys, key):
    # ### Naive dead lock detect:
    #
    # Locks must be acquired in alphabetic order(or other order, from left to right.
    #     Trying to acquire a `lock>=right_most_locked`, is a forward locking Otherwise it
    #                                                                                   is a backward locking.
    #
    #     Always do forward locking we can guarantee there won't be a dead lock.
    # Since a deadlock needs at least one backward locking to form a circular dependency.
    #
    #     If a process fails to acquire a lock in a backward locking,
    #     it should release all locks it holds and redo the entire transaction.
    #
    #     E.g. suppose X has acquired lock a and c, Y has acquired lock b:
    #
    # ```
    # Acquired locks by process X and Y
    # locks are ordered left-to-right
    #                   ---------------------------------------------
    # proc-X   a(locked)                  c(locked)
    # proc-Y               b(locked)
    # ```
    #
    # If
    # X tries to acquire b(**backward**),
    #                    Y tries to acquire c(forward):
    # There is a deadlock. X should release all locks.
    #
    # ```
    # ---------------------------------------------
    # proc-X   a(locked)   .------------ c(locked)
    # v             ^
    # proc-Y               b(locked) ----'
    # ```
    #
    # If
    # X tries to acquire b(**backward**)
    # Y tries to acquires a(**backward**)
    # There is a deadlock, X and Y should both release their locks.
    #
    # ```
    # ---------------------------------------------
    # proc-X   a(locked)   .------------ c(locked)
    #          ^           v
    # proc-Y   '---------- b(locked)
    """
    Check if the operation of locking `key` is a backward-locking.
    :param locked_keys: is a collection support `in` operator that contains already locked keys.
    :param key: is the key to lock.
    :return: a `bool` indicate if locking `key` would be a backward-locking.
    """
    locked_keys = sorted(locked_keys)
    assert key not in locked_keys, "must not re-lock a key"

    if len(locked_keys) == 0:
        is_backward = False
    else:
        is_backward = key < locked_keys[-1]

    return is_backward


def _init_node(zkcli, parent_path, node, val, acl, users):
    path = parent_path + "/" + node

    if acl is None:
        acls = zkcli.get_acls(parent_path)[0]
    else:
        acls = [(user, users[user], perms) for user, perms in acl.items()]
        acls = make_kazoo_digest_acl(acls)

    if zkcli.exists(path) is None:
        zkcli.create(path, value=val, acl=acls)
    else:
        zkcli.set_acls(path, acls)

    return path


def init_hierarchy(hosts, hierarchy, users, auth):
    """
    It initialize a zookeeper cluster, including initializing the tree structure,
    setting value and permissions for each node.
    :param hosts: comma-separated list of hosts to connect to, such as `'127.0.0.1:2181,127.0.0.1:2182,[::1]:2183'`.
    :param hierarchy: a dict of zk node structure with value or acl optional for each node.
    For example,
    """
    # node1:
    #   __val__: "json, optional, by default a '{}'"
    #   __acl__: # optional, same with parent if not given
    #     user_1: "cdrwa"
    #     user_2: "rw"
    #                 ...
    # node2:
    #     node21: {...}
    """
    As shown above, each node has two attributes `__val__` and `__acl__` which are used to set the corresponding node.
    :param users:  a dict in form `{<username>: <password>}` containing all users in `hierarchy`.
    :param auth: a tuple in form `(<scheme>, <user>, <password>)`.
    It is the authorization info to connect to zookeeper which is used to initialize the zookeeper cluster.
    :return: None.
    """
    zkcli = KazooClient(hosts)
    zkcli.start()

    scheme, name, passw = auth
    zkcli.add_auth(scheme, name + ":" + passw)

    def _init_hierarchy(hierarchy, parent_path):
        if len(hierarchy) == 0:
            return

        for node, attr_children in hierarchy.items():
            val = attr_children.get("__val__", {})
            val = k3utfjson.dump(val).encode("utf-8")
            acl = attr_children.get("__acl__")

            path = _init_node(zkcli, parent_path, node, val, acl, users)
            children = {k: v for k, v in attr_children.items() if k not in ("__val__", "__acl__")}

            _init_hierarchy(children, path)

    _init_hierarchy(hierarchy, "/")
    close_zk(zkcli)


def _make_zk_path(*paths):
    return "/" + "/".join([x.strip("/") for x in paths])


def export_hierarchy(zkcli, zkpath):
    """
    Exporting a zookeeper node in a tree structure, and you can import the data into zookeeper
    using `zkutil.init_hierarchy`
    :param zkcli: is a `KazooClient` instance connected to zk.
    :param zkpath: is zookeeper root path that you want export
    :return:
    """
    if zkpath != "/":
        zkpath = zkpath.rstrip("/")

    if not zkpath.startswith("/"):
        raise ZkPathError("zkpath: {0} Error, Should be absolute path".format(zkpath))

    zk_node = _export_hierarchy(zkcli, zkpath)

    return zk_node


def _export_hierarchy(zkcli, zkpath):
    acls = {}

    value, stat = zkcli.get(zkpath)

    _acls, stat = zkcli.get_acls(zkpath)
    _acls = parse_kazoo_acl(_acls)

    for schema, user, perm in _acls:
        acls.update({user: perm})

    _zk_node = {"__val__": value, "__acl__": acls}

    for child in zkcli.get_children(zkpath):
        _zkpath = _make_zk_path(zkpath, child)

        _zk_node[child] = _export_hierarchy(zkcli, _zkpath)

    return _zk_node


NeedWait = namedtuple("NeedWait", [])


# A collection of helper functions those block until a condition satisfied before
# returning.
# Such as:
#
# -   Wait for a zk-node to be absent.
# -   Wait for a zk-node version greater than a specified one before returning
# the node value.
#
# These methods share the same form: `def xxx(zkclient, path, timeout=None, **kwargs)`.
#
# Common parameters are:
#
# -   `zkclient`:
# kazoo client.
#
# -   `path`:
# specifies the path to watch.
#
# -   `timeout`:
# specifies the time(in second) to wait.
#
# By default it is `None` which means to wait for a year.
#
# If condition was not satisfied in `timeout` seconds,
# it raises a `ZKWaitTimeout` exception.
def _conditioned_get_loop(zkclient, path, conditioned_get, timeout=None, **kwargs):
    if timeout is None:
        timeout = 86400 * 365

    expire_at = time.time() + timeout
    lck = threading.RLock()
    maybe_available = threading.Event()
    maybe_available.clear()

    def set_available():
        with lck:
            maybe_available.set()

    def on_connection_change(state):
        # notify it to re-get, then raise Connection related error
        logger.info("connection state change: {0}".format(state))
        set_available()

    zkclient.add_listener(on_connection_change)

    try:
        while True:
            it = conditioned_get(zkclient, path, **kwargs)
            next(it)
            rst = it.send((NeedWait, set_available))

            if rst is not NeedWait:
                return rst

            if maybe_available.wait(expire_at - time.time()):
                with lck:
                    maybe_available.clear()
                continue

            raise ZKWaitTimeout(
                "timeout({timeout} sec) waiting for {path} to satisfy: {cond}".format(
                    timeout=timeout, path=path, cond=str(kwargs)
                )
            )
    finally:
        try:
            zkclient.remove_listener(on_connection_change)
        except Exception as e:
            logger.info(repr(e) + " while removing on_connection_change")


def make_conditioned_getter(conditioned_get):
    def _wrap(zkclient, path, timeout=None, **kwargs):
        return _conditioned_get_loop(zkclient, path, conditioned_get, timeout=timeout, **kwargs)

    _wrap.__name__ = conditioned_get.__name__

    return _wrap


@make_conditioned_getter
def get_next(zkclient, path, version=-1):
    """
    Wait until zk-node `path` version becomes greater than `version` then return
    node value and `zstat`.
    :param version: the version that `path` version must be greater than.
    :return: zk node value and `zstat`.
    """
    NeedWait, set_available = yield

    val, zstat = zkclient.get(path, watch=lambda watchevent: set_available())
    if zstat.version > version:
        yield val, zstat

    yield NeedWait


@make_conditioned_getter
def wait_absent(zkclient, path):
    """
    Wait at most `timeout` seconds for zk-node `path` to be absent.

    If `path` does not exist, it returns at once.
    :param zkclient:
    :param path:
    :return:
    """
    NeedWait, set_available = yield

    try:
        zkclient.get(path, watch=lambda watchevent: set_available())
    except NoNodeError as e:
        logger.info(repr(e) + " found, return")
        yield None

    yield NeedWait
