# command line interface

import sys
import datetime
import asyncclick as click
from collections.abc import Mapping

from distkv.util import MsgReader, MsgWriter
from distkv.util import yprint, PathLongener, P, yload, Path
from distkv.codec import unpacker
from distmqtt.codecs import MsgPackCodec


@click.group(short_help="Manage data.")  # pylint: disable=undefined-variable
async def cli():
    """
    Low-level tools that don't depend on a running DistKV server.
    """
    pass


@cli.command("cfg")
@click.argument("path", nargs=1)
@click.pass_obj
async def cfg_dump(obj, path):
    """Emit the current configuration as a YAML file.

    You can limit the output by path elements.
    E.g., "cfg connect.host" will print "localhost".

    Single values are printed with a trailing line feed.

    Dump the whole config with "… dump cfg :".
    """
    cfg = obj.cfg
    for p in P(path):
        try:
            cfg = cfg[p]
        except KeyError:
            if obj.debug:
                print("Unknown:", p)
            sys.exit(1)
    if isinstance(cfg, str):
        print(cfg, file=obj.stdout)
    else:
        yprint(cfg, stream=obj.stdout)


@cli.command("file")
@click.option("-p", "--path", is_flag=True, default=False, help="Unwrap paths")
@click.option("-f", "--filter", "filter_", multiple=True, help="Only emit entries with this path")
@click.argument("file", nargs=1)
@click.pass_obj
async def file_(obj, file, path, filter_):
    """Read a MsgPack file and dump as YAML."""
    if path or filter_:
        pl = PathLongener()
    else:
        pl = lambda _: None
    filter_ = [P(x) for x in filter_]
    async with MsgReader(path=file) as f:
        async for msg in f:
            pl(msg)
            if filter_:
                if "path" not in msg:
                    continue
                for f in filter_:
                    if msg.path[: len(f)] == f:
                        break
                else:
                    continue
            yprint(msg, stream=obj.stdout)
            print("---", file=obj.stdout)


@cli.command("yaml")
@click.argument("msgpack", nargs=1)
async def yaml_(msgpack):
    """Read a multi-part YAML file from stdin and dump as msgpack stream."""
    async with MsgWriter(path=msgpack) as f:
        for d in yload(sys.stdin, multi=True):
            await f(d)


@cli.command()
@click.argument("node", nargs=1)
@click.argument("file", type=click.Path(), nargs=1)
async def init(node, file):
    """Write an initial preload file.

    Usage: distkv dump init <node> <outfile>

    Writes an initial DistKV file that behaves as if it was generated by <node>.

    Using this command, followed by "distkv server -l <outfile> <node>", is
    equivalent to running "distkv server -i 'Initial data' <node>.
    """
    async with MsgWriter(path=file) as f:
        await f(
            dict(
                chain=dict(node=node, tick=1, prev=None),
                depth=0,
                path=[],
                tock=1,
                value="Initial data",
            )
        )


@cli.command("msg")
@click.argument("path", nargs=1)
@click.pass_obj
async def msg_(obj, path):
    """
    Monitor the server-to-sever message stream.

    Use ':' for the main server's "update" stream.
    Use '+NAME' to monitor a different stream instead.
    Use '+' to monitor all streams.
    Otherwise use the given name as-is; Mosquitto wildcard rules apply.

    \b
    Common streams (prefix with '+'):
    * ping    all servers
    * update  data changes
    * del     nodes responsible for cleaning up deleted records
    """
    from distkv.backend import get_backend

    class _Unpack:
        def __init__(self):
            self._part_cache = dict()

    import distkv.server

    _Unpack._unpack_multiple = distkv.server.Server._unpack_multiple
    _unpacker = _Unpack()._unpack_multiple

    path = P(path)
    if len(path) == 0:
        path = P(obj.cfg.server.root) | "update"
    elif len(path) == 1 and path[0].startswith("+"):
        p = path[0][1:]
        path = P(obj.cfg.server.root)
        path |= p or "#"
    be = obj.cfg.server.backend
    kw = obj.cfg.server[be]

    async with get_backend(be)(**kw) as conn:
        async with conn.monitor(*path) as stream:
            async for msg in stream:
                v = vars(msg)
                if isinstance(v.get("payload"), (bytearray, bytes)):
                    t = msg.topic
                    v = unpacker(v["payload"])
                    v = _unpacker(v)
                    if v is None:
                        continue
                    if not isinstance(v, Mapping):
                        v = {"_data": v}
                    v["_topic"] = Path.build(t)
                else:
                    v["_type"] = type(msg).__name__
                v["_timestamp"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

                yprint(v, stream=obj.stdout)
                print("---", file=obj.stdout)


@cli.command("post")
@click.argument("path", nargs=1)
@click.pass_obj
async def post_(obj, path):
    """
    Send a msgpack-encoded message (or several) to a specific MQTT topic.

    Messages are read from YAML.
    Common streams:
    * ping: sync: all servers (default)
    * update: data changes
    * del: sync: nodes responsible for cleaning up deleted records
    """
    from distkv.backend import get_backend

    path = P(path)
    be = obj.cfg.server.backend
    kw = obj.cfg.server[be]

    async with get_backend(be)(codec=MsgPackCodec, **kw) as conn:
        for d in yload(sys.stdin, multi=True):
            topic = d.pop("_topic", path)
            await conn.send(*topic, payload=d)
