#!/usr/bin/python
# Copyright 2015 WP Engine, Inc. All rights reserved.
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import annotations


DOCUMENTATION = r"""
module: znode
short_description: Create, delete, retrieve, and update znodes using ZooKeeper
description:
  - Create, delete, retrieve, and update znodes using ZooKeeper.
attributes:
  check_mode:
    support: none
  diff_mode:
    support: none
extends_documentation_fragment:
  - community.general.attributes
options:
  hosts:
    description:
      - A list of ZooKeeper servers (format V([server]:[port])).
    required: true
    type: str
  name:
    description:
      - The path of the znode.
    required: true
    type: str
  value:
    description:
      - The value assigned to the znode.
    type: str
  op:
    description:
      - An operation to perform. Mutually exclusive with state.
    choices: [get, wait, list]
    type: str
  state:
    description:
      - The state to enforce. Mutually exclusive with op.
    choices: [present, absent]
    type: str
  timeout:
    description:
      - The amount of time to wait for a node to appear.
    default: 300
    type: int
  recursive:
    description:
      - Recursively delete node and all its children.
    type: bool
    default: false
  auth_scheme:
    description:
      - Authentication scheme.
    choices: [digest, sasl]
    type: str
    default: "digest"
    version_added: 5.8.0
  auth_credential:
    description:
      - The authentication credential value. Depends on O(auth_scheme).
      - The format for O(auth_scheme=digest) is C(user:password), and the format for O(auth_scheme=sasl) is C(user:password).
    type: str
    version_added: 5.8.0
  use_tls:
    description:
      - Using TLS/SSL or not.
    type: bool
    default: false
    version_added: '6.5.0'
requirements:
  - kazoo >= 2.1
author: "Trey Perry (@treyperry)"
"""

EXAMPLES = r"""
- name: Creating or updating a znode with a given value
  community.general.znode:
    hosts: 'localhost:2181'
    name: /mypath
    value: myvalue
    state: present

- name: Getting the value and stat structure for a znode
  community.general.znode:
    hosts: 'localhost:2181'
    name: /mypath
    op: get

- name: Getting the value and stat structure for a znode using digest authentication
  community.general.znode:
    hosts: 'localhost:2181'
    auth_credential: 'user1:s3cr3t'
    name: /secretmypath
    op: get

- name: Listing a particular znode's children
  community.general.znode:
    hosts: 'localhost:2181'
    name: /zookeeper
    op: list

- name: Waiting 20 seconds for a znode to appear at path /mypath
  community.general.znode:
    hosts: 'localhost:2181'
    name: /mypath
    op: wait
    timeout: 20

- name: Deleting a znode at path /mypath
  community.general.znode:
    hosts: 'localhost:2181'
    name: /mypath
    state: absent

- name: Creating or updating a znode with a given value on a remote Zookeeper
  community.general.znode:
    hosts: 'my-zookeeper-node:2181'
    name: /mypath
    value: myvalue
    state: present
  delegate_to: 127.0.0.1
"""

import time
import traceback

KAZOO_IMP_ERR = None
try:
    from kazoo.client import KazooClient
    from kazoo.handlers.threading import KazooTimeoutError

    KAZOO_INSTALLED = True
except ImportError:
    KAZOO_IMP_ERR = traceback.format_exc()
    KAZOO_INSTALLED = False

from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.common.text.converters import to_bytes


def main():
    module = AnsibleModule(
        argument_spec=dict(
            hosts=dict(required=True, type="str"),
            name=dict(required=True, type="str"),
            value=dict(type="str"),
            op=dict(choices=["get", "wait", "list"]),
            state=dict(choices=["present", "absent"]),
            timeout=dict(default=300, type="int"),
            recursive=dict(default=False, type="bool"),
            auth_scheme=dict(default="digest", choices=["digest", "sasl"]),
            auth_credential=dict(type="str", no_log=True),
            use_tls=dict(default=False, type="bool"),
        ),
        supports_check_mode=False,
    )

    if not KAZOO_INSTALLED:
        module.fail_json(msg=missing_required_lib("kazoo >= 2.1"), exception=KAZOO_IMP_ERR)

    check = check_params(module.params)
    if not check["success"]:
        module.fail_json(msg=check["msg"])

    zoo = KazooCommandProxy(module)
    try:
        zoo.start()
    except KazooTimeoutError:
        module.fail_json(msg="The connection to the ZooKeeper ensemble timed out.")

    command_dict = {
        "op": {"get": zoo.get, "list": zoo.list, "wait": zoo.wait},
        "state": {"present": zoo.present, "absent": zoo.absent},
    }

    command_type = "op" if "op" in module.params and module.params["op"] is not None else "state"
    method = module.params[command_type]
    result, result_dict = command_dict[command_type][method]()
    zoo.shutdown()

    if result:
        module.exit_json(**result_dict)
    else:
        module.fail_json(**result_dict)


def check_params(params):
    if not params["state"] and not params["op"]:
        return {"success": False, "msg": "Please define an operation (op) or a state."}

    if params["state"] and params["op"]:
        return {"success": False, "msg": "Please choose an operation (op) or a state, but not both."}

    return {"success": True}


class KazooCommandProxy:
    def __init__(self, module):
        self.module = module
        self.zk = KazooClient(module.params["hosts"], use_ssl=module.params["use_tls"])

    def absent(self):
        return self._absent(self.module.params["name"])

    def exists(self, znode):
        return self.zk.exists(znode)

    def list(self):
        children = self.zk.get_children(self.module.params["name"])
        return True, {
            "count": len(children),
            "items": children,
            "msg": "Retrieved znodes in path.",
            "znode": self.module.params["name"],
        }

    def present(self):
        return self._present(self.module.params["name"], self.module.params["value"])

    def get(self):
        return self._get(self.module.params["name"])

    def shutdown(self):
        self.zk.stop()
        self.zk.close()

    def start(self):
        self.zk.start()
        if self.module.params["auth_credential"]:
            self.zk.add_auth(self.module.params["auth_scheme"], self.module.params["auth_credential"])

    def wait(self):
        return self._wait(self.module.params["name"], self.module.params["timeout"])

    def _absent(self, znode):
        if self.exists(znode):
            self.zk.delete(znode, recursive=self.module.params["recursive"])
            return True, {"changed": True, "msg": "The znode was deleted."}
        else:
            return True, {"changed": False, "msg": "The znode does not exist."}

    def _get(self, path):
        if self.exists(path):
            value, zstat = self.zk.get(path)
            stat_dict = {}
            for i in dir(zstat):
                if not i.startswith("_"):
                    attr = getattr(zstat, i)
                    if isinstance(attr, (int, str)):
                        stat_dict[i] = attr
            result = True, {"msg": "The node was retrieved.", "znode": path, "value": value, "stat": stat_dict}
        else:
            result = False, {"msg": "The requested node does not exist."}

        return result

    def _present(self, path, value):
        if self.exists(path):
            (current_value, zstat) = self.zk.get(path)
            if value != current_value:
                self.zk.set(path, to_bytes(value))
                return True, {"changed": True, "msg": "Updated the znode value.", "znode": path, "value": value}
            else:
                return True, {"changed": False, "msg": "No changes were necessary.", "znode": path, "value": value}
        else:
            self.zk.create(path, to_bytes(value), makepath=True)
            return True, {"changed": True, "msg": "Created a new znode.", "znode": path, "value": value}

    def _wait(self, path, timeout, interval=5):
        lim = time.time() + timeout

        while time.time() < lim:
            if self.exists(path):
                return True, {
                    "msg": "The node appeared before the configured timeout.",
                    "znode": path,
                    "timeout": timeout,
                }
            else:
                time.sleep(interval)

        return False, {
            "msg": "The node did not appear before the operation timed out.",
            "timeout": timeout,
            "znode": path,
        }


if __name__ == "__main__":
    main()
