"""Commands that drive the pet's *body* instead of the shell.

The server relays shell commands to this machine (see server_client.
run_local_command). Rather than inventing a new payload type the desk API
doesn't speak — this client can't change the server — a small `petctl`
pseudo-command is intercepted before it ever reaches `subprocess`: if Bolt
emits `petctl move top-left` or `petctl emote wave`, the pet does it and
returns a normal-looking command output string, so from the server's side
it's just another tool call that worked.

Pure parsing logic — no Qt, no subprocess — so it's cheap to unit test. The
UI half lives in ui/pet_window.py (apply_action).
"""

from __future__ import annotations

import shlex
from typing import Optional

# What Bolt is allowed to type. Anything else falls through to a real shell.
_PREFIXES = ("petctl", "bolt-pet", "pet")

ANCHORS = (
    "top-left", "top", "top-right",
    "left", "center", "right",
    "bottom-left", "bottom", "bottom-right",
    "cursor", "random",
)

EMOTES = ("wave", "hop", "spin", "nod", "shake", "bounce", "wiggle")

# Where `petctl jump` can be aimed. A bare number (1-based) works too, as does
# any unique part of a monitor's name — resolution lives in monitors.resolve().
MONITOR_SPECS = (
    "next", "prev", "primary", "other", "random",
    "left", "right", "up", "down",
)

HELP = (
    "petctl move <x> <y> | <" + "|".join(ANCHORS) + ">\n"
    "petctl jump <monitor number|" + "|".join(MONITOR_SPECS) + "|name>\n"
    "petctl monitors\n"
    "petctl read [monitor number|here|all]\n"
    "petctl emote <" + "|".join(EMOTES) + ">\n"
    "petctl say <text>\n"
    "petctl wander on|off\n"
    "petctl nap on|off\n"
    "petctl voice reset\n"
    "petctl self_restart [why]"
)

# `petctl voice` only ever goes one way: back to the configured voice. Picking
# a *different* one is the server's job (its speak_as reply marker), and it
# already knows how — what it has no way to say is "never mind, be yourself
# again", because it was never told which voice that is.
VOICE_RESETS = ("reset", "default", "normal", "own", "back", "mine", "yours")


class ActionError(Exception):
    """Bad petctl syntax — reported back to the server as command output."""


def is_pet_command(command: str) -> bool:
    parts = (command or "").strip().split()
    return bool(parts) and parts[0].lower() in _PREFIXES


def _bool_arg(value: str) -> bool:
    value = value.lower()
    if value in ("on", "true", "yes", "1", "start", "enable"):
        return True
    if value in ("off", "false", "no", "0", "stop", "disable"):
        return False
    raise ActionError(f"expected on/off, got {value!r}")


def parse(command: str) -> Optional[dict]:
    """Parse a `petctl ...` string into an action dict, or None if this isn't
    a pet command at all (caller should run it as a real shell command).
    Raises ActionError on a pet command that doesn't make sense."""
    if not is_pet_command(command):
        return None
    try:
        parts = shlex.split(command.strip())
    except ValueError as exc:  # unbalanced quotes
        raise ActionError(f"couldn't parse arguments: {exc}") from exc
    verb = (parts[1].lower() if len(parts) > 1 else "help")
    args = parts[2:]

    if verb in ("help", "-h", "--help"):
        return {"action": "help"}

    if verb in ("move", "goto", "walk"):
        if not args:
            raise ActionError("move needs a target: " + ", ".join(ANCHORS) + ", or x y")
        if len(args) >= 2 and _looks_numeric(args[0]) and _looks_numeric(args[1]):
            return {"action": "move", "x": int(float(args[0])), "y": int(float(args[1]))}
        anchor = args[0].lower().replace("_", "-")
        if anchor not in ANCHORS:
            raise ActionError(f"unknown position {args[0]!r}; try one of: " + ", ".join(ANCHORS))
        return {"action": "move", "anchor": anchor}

    if verb in ("jump", "monitor", "screen"):
        if not args:
            raise ActionError(
                "jump needs a monitor: a number, a name, or one of "
                + ", ".join(MONITOR_SPECS)
            )
        # The spec isn't validated here on purpose: which monitors exist is a
        # runtime fact this pure module doesn't have. monitors.resolve() does
        # it once the published screen list is in hand.
        return {"action": "jump", "target": " ".join(args).strip()}

    if verb in ("monitors", "screens", "displays"):
        return {"action": "monitors"}

    if verb in ("read", "look", "ocr", "see"):
        target = (" ".join(args).strip() or "here").lower()
        return {"action": "read", "target": target}

    if verb in ("emote", "do"):
        if not args:
            raise ActionError("emote needs a name: " + ", ".join(EMOTES))
        emote = args[0].lower()
        if emote not in EMOTES:
            raise ActionError(f"unknown emote {args[0]!r}; try one of: " + ", ".join(EMOTES))
        return {"action": "emote", "emote": emote}

    if verb == "say":
        text = " ".join(args).strip()
        if not text:
            raise ActionError("say needs something to say")
        return {"action": "say", "text": text}

    if verb == "wander":
        if not args:
            raise ActionError("wander needs on or off")
        return {"action": "wander", "enabled": _bool_arg(args[0])}

    if verb == "voice":
        target = (args[0].lower() if args else "reset").lstrip("-")
        if target not in VOICE_RESETS:
            raise ActionError(
                f"can't set a voice from petctl (got {args[0]!r}); "
                "use the speak_as reply marker to pick one. "
                "petctl voice reset goes back to the default voice."
            )
        return {"action": "voice", "voice": "default"}

    if verb in ("self_restart", "restart", "reboot"):
        # Free text, not a fixed grammar: the argument is a note to the pet's
        # *next* process about why it died and what to look at when it comes
        # back, so anything the model wants to tell future-itself is valid.
        return {"action": "self_restart", "reason": " ".join(args).strip()}

    if verb in ("nap", "sleep", "dnd"):
        if not args:
            raise ActionError("nap needs on or off")
        return {"action": "nap", "enabled": _bool_arg(args[0])}

    raise ActionError(f"unknown petctl verb {verb!r}\n{HELP}")


def _looks_numeric(value: str) -> bool:
    try:
        float(value)
        return True
    except ValueError:
        return False


def describe(action: dict) -> str:
    """The text handed back to the server as this "command"'s output. Phrased
    as a completed fact so the model doesn't narrate the mechanics of it."""
    kind = action.get("action")
    if kind == "move":
        where = action.get("anchor") or f"({action.get('x')}, {action.get('y')})"
        return f"[pet] walking to {where}"
    if kind == "jump":
        return f"[pet] jumping to monitor {action['target']}"
    if kind == "emote":
        return f"[pet] {action['emote']}"
    if kind == "say":
        return "[pet] showing that in the speech bubble"
    if kind == "wander":
        return "[pet] wandering " + ("enabled" if action["enabled"] else "disabled")
    if kind == "nap":
        return "[pet] " + ("napping" if action["enabled"] else "awake")
    if kind == "help":
        return HELP
    return "[pet] ok"
