"""Direct tool invocation from chat input: `/tool_name(arg=val, arg=val)`.

A human typing this IS the approval — there's no one else to ask — so a
recognized slash-command skips the ask-policy round-trip entirely and
dispatches straight through `tools.dispatch()`, the same entry point a
model-issued tool call already goes through. It does not bypass anything a
tool validates internally (path boundaries, size caps, Curry's own sandbox
checks, etc.) — only the human-approval step, which this message already is.

Argument values are parsed with `ast.literal_eval`, not `eval()`: strings,
numbers, booleans, None, and literal lists/dicts/tuples only. There is no way
to reference a name, call a function, or access an attribute in this syntax —
a malformed or hostile-looking argument fails to parse rather than executing
anything, which is the "lint, not run" property that makes this different
from just typing Python.

The whole message must be nothing but the command — this is a deliberate
command line, not a directive embedded in prose. Anything else (including a
message that merely starts with `/` but isn't shaped like this) falls through
to the normal chat/model path unchanged.
"""
from __future__ import annotations

import ast
import re
from dataclasses import dataclass
from typing import Any, Optional

# name(args) where name is a plain identifier — the same shape as a Python
# function call, so it reads the way the tool's own schema already documents
# it. re.DOTALL: argument values (e.g. a multi-line body= string) may
# legitimately contain newlines.
_COMMAND_RE = re.compile(r"^/([A-Za-z_][A-Za-z0-9_]*)\((.*)\)\s*$", re.DOTALL)


@dataclass
class SlashCommand:
    tool: str
    args: dict[str, Any]


@dataclass
class SlashCommandError:
    text: str


def parse_slash_command(message: str) -> Optional[SlashCommand | SlashCommandError]:
    """Parse `/tool_name(arg=val, ...)`.

    Returns None when `message` isn't shaped like a slash-command at all (the
    caller should treat it as an ordinary chat message). Returns
    SlashCommandError when it looks like one but is malformed — that's worth
    telling the user about rather than silently sending "/curry_call_fnction(...)"
    to the model as if it were prose.
    """
    stripped = (message or "").strip()
    match = _COMMAND_RE.match(stripped)
    if not match:
        return None

    tool_name, raw_args = match.group(1), match.group(2).strip()
    if not raw_args:
        return SlashCommand(tool=tool_name, args={})

    # Parse "k1=v1, k2=v2" as keyword arguments to a call with no positional
    # arguments and no function to actually call — ast.parse(mode='eval') on a
    # synthetic call expression reuses Python's own keyword-argument grammar
    # (quoting, nesting, trailing commas) instead of hand-rolling a parser for
    # it, while call() as a bare name is never resolved or invoked.
    try:
        tree = ast.parse(f"call({raw_args})", mode="eval")
    except SyntaxError as e:
        return SlashCommandError(f"could not parse arguments for /{tool_name}(...): {e}")

    call_node = tree.body
    if not isinstance(call_node, ast.Call) or call_node.args:
        return SlashCommandError(
            f"/{tool_name}(...) arguments must be keyword form: arg=value, arg=value"
        )

    args: dict[str, Any] = {}
    for kw in call_node.keywords:
        if kw.arg is None:  # **mapping unpacking — no source for that here
            return SlashCommandError(f"/{tool_name}(...) does not support **-unpacking")
        try:
            args[kw.arg] = ast.literal_eval(kw.value)
        except (ValueError, SyntaxError):
            return SlashCommandError(
                f"/{tool_name}(...): argument '{kw.arg}' must be a literal "
                "(string, number, bool, None, list, dict, or tuple) — not an "
                "expression, name, or call"
            )

    return SlashCommand(tool=tool_name, args=args)
