# command line interface
from __future__ import annotations

import json

import asyncclick as click
from moat.util import NotGiven, P, Path, PathLongener, yload, yprint
import contextlib


@click.group()  # pylint: disable=undefined-variable
async def cli():
    """Manage types and type matches. Usage: … type …"""
    pass


@cli.command()
@click.option("-s", "--script", type=click.File(mode="w", lazy=True), help="Save the script here")
@click.option("-S", "--schema", type=click.File(mode="w", lazy=True), help="Save the schema here")
@click.option("-y", "--yaml", "yaml_", is_flag=True, help="Write schema as YAML. Default: JSON.")
@click.argument("path", type=P, nargs=1)
@click.pass_obj
async def get(obj, path, script, schema, yaml_):
    """Read type checker information"""
    if not len(path):
        raise click.UsageError("You need a non-empty path.")
    res = await obj.client._request(
        action="get_internal",
        path=Path("type") + path,
        iter=False,
        nchain=obj.meta,
    )
    try:
        r = res.value
    except AttributeError:
        raise click.UsageError(f"No data at {Path('type') + path}") from None

    if not obj.meta:
        res = res.value
    if script:
        script.write(r.pop("code"))
    if schema:
        if yaml_:
            yprint(r.pop("schema"), stream=schema)
        else:
            json.dump(r.pop("schema"), schema)
    yprint(res, stream=obj.stdout)


@cli.command("set")
@click.option("-g", "--good", multiple=True, help="Example for passing values")
@click.option("-b", "--bad", multiple=True, help="Example for failing values")
@click.option("-d", "--data", type=click.File(mode="r"), help="Load metadata from this YAML file.")
@click.option("-s", "--script", type=click.File(mode="r"), help="File with the checking script")
@click.option("-S", "--schema", type=click.File(mode="r"), help="File with the JSON schema")
@click.option("-y", "--yaml", "yaml_", is_flag=True, help="load the schema as YAML. Default: JSON")
@click.argument("path", type=P, nargs=1)
@click.pass_obj
async def set_(obj, path, good, bad, script, schema, yaml_, data):
    """Write type checker information."""
    if not len(path):
        raise click.UsageError("You need a non-empty path.")

    if data:
        msg = yload(data)
    else:
        msg = {}
    chain = NotGiven
    if "value" in msg:
        chain = msg.get("chain", NotGiven)
        msg = msg["value"]

    msg.setdefault("good", [])
    msg.setdefault("bad", [])
    for x in good:
        msg["good"].append(eval(x))  # pylint: disable=eval-used
    for x in bad:
        msg["bad"].append(eval(x))  # pylint: disable=eval-used

    if "code" in msg:
        if script:
            raise click.UsageError("Duplicate script")
    elif script:
        msg["code"] = script.read()

    if "schema" in msg:
        raise click.UsageError("Missing schema")
    elif schema:
        if yaml_:
            msg["schema"] = yload(schema)
        else:
            msg["schema"] = json.load(schema)

    if "schema" not in msg and "code" not in msg:
        raise click.UsageError("I need a schema, Python code, or both.")

    if len(msg["good"]) < 2:
        raise click.UsageError("Missing known-good test values (at least two)")
    if not msg["bad"]:
        raise click.UsageError("Missing known-bad test values")

    res = await obj.client._request(
        action="set_internal",
        value=msg,
        path=Path("type") + path,
        iter=False,
        nchain=obj.meta,
        **({} if chain is NotGiven else {"chain": chain}),
    )
    if obj.meta:
        yprint(res, stream=obj.stdout)


@cli.command()
@click.option("-R", "--raw", is_flag=True, help="Print just the path.")
@click.option("-t", "--type", "type_", help="Type path to link to.")
@click.option("-d", "--delete", help="Use to delete this mapping.")
@click.argument("path", type=P, nargs=1)
@click.pass_obj
async def match(obj, path, type_, delete, raw):  # pylint: disable=redefined-builtin
    """Match a type to a path (read, if no type given; list if empty path)"""
    if not len(path):
        if raw or type_ or delete:
            raise click.UsageError("No options allowed when dumping the match tree..")
        y = {}
        pl = PathLongener()
        async for r in await obj.client._request(
            "get_tree_internal",
            path=Path("match") + path,
            iter=True,
            nchain=0,
        ):
            pl(r)
            path = r["path"]
            yy = y
            for p in path:
                yy = yy.setdefault(p, {})
            with contextlib.suppress(KeyError):
                yy["_"] = r["value"]
        yprint(y, stream=obj.stdout)
        return

    if type_ and delete:
        raise click.UsageError("You can't both set and delete a path.")
    if raw and (type_ or delete):
        raise click.UsageError("You can only print the raw path when reading a match.")

    if delete:
        res = await obj.client._request(action="delete_internal", path=Path("type") + path)
        if obj.meta:
            yprint(res, stream=obj.stdout)
        return

    msg = {}
    if type_:
        msg["type"] = P(type_)
        act = "set_internal"
    elif delete:
        act = "delete_internal"
    else:
        act = "get_internal"
    res = await obj.client._request(
        action=act,
        value=msg,
        path=Path("match") + path,
        iter=False,
        nchain=obj.meta,
    )
    if obj.meta:
        yprint(res, stream=obj.stdout)
    elif type_ or delete:
        pass
    else:
        print(" ".join(str(x) for x in res.type), file=obj.stdout)


@cli.command()
@click.argument("path", type=P, nargs=1)
@click.pass_obj
async def list(obj, path):  # pylint: disable=redefined-builtin
    """Dump type data"""

    y = {}
    pl = PathLongener()
    async for r in await obj.client._request(
        "get_tree_internal",
        path=Path("type") + path,
        iter=True,
        nchain=0,
    ):
        pl(r)
        path = r["path"]
        yy = y
        for p in path:
            yy = yy.setdefault(p, {})
        with contextlib.suppress(KeyError):
            yy["_"] = r["value"]
    yprint(y, stream=obj.stdout)
