# retoor <retoor@molodetz.nl>

from __future__ import annotations

import json
import logging
from typing import Any, Awaitable, Callable, Optional

logger = logging.getLogger("devii.client")

RequestSink = Callable[[str, dict[str, Any]], Awaitable[Any]]

UNAVAILABLE = {
    "status": "unavailable",
    "message": (
        "No browser is attached to this session. Client-side actions only work in the web "
        "terminal, with a live connection open."
    ),
}
EVAL_DISABLED = {
    "status": "disabled",
    "message": "JavaScript execution is disabled for Devii by the administrator.",
}
CONTROL_DISABLED = {
    "status": "disabled",
    "message": "Browser control is disabled for Devii by the administrator.",
}

CONTROL_ACTIONS = frozenset(
    {
        "click_element",
        "fill_field",
        "set_control",
        "submit_form",
        "press_key",
        "run_sequence",
    }
)


class ClientController:
    def __init__(self, allow_eval: bool = True, allow_control: bool = True) -> None:
        self._request: Optional[RequestSink] = None
        self._allow_eval = allow_eval
        self._allow_control = allow_control

    def bind(self, request: RequestSink) -> None:
        self._request = request
        logger.debug("Client controller bound to a session")

    def unbind(self) -> None:
        self._request = None
        logger.debug("Client controller unbound")

    @property
    def attached(self) -> bool:
        return self._request is not None

    async def dispatch(self, name: str, arguments: dict[str, Any]) -> str:
        if name == "run_js" and not self._allow_eval:
            return json.dumps(EVAL_DISABLED, ensure_ascii=False)
        if name in CONTROL_ACTIONS and not self._allow_control:
            return json.dumps(CONTROL_DISABLED, ensure_ascii=False)
        if self._request is None:
            return json.dumps(UNAVAILABLE, ensure_ascii=False)
        try:
            result = await self._request(name, arguments)
        except Exception as exc:  # noqa: BLE001 - surfaced to the model as data
            logger.exception("Client action %s failed", name)
            return json.dumps(
                {"status": "error", "action": name, "message": str(exc)},
                ensure_ascii=False,
            )

        if isinstance(result, dict) and result.get("error"):
            return json.dumps(
                {"status": "error", "action": name, "message": str(result["error"])},
                ensure_ascii=False,
            )
        return json.dumps(
            {"status": "success", "action": name, "result": result}, ensure_ascii=False
        )
