#!/usr/bin/env python3
"""Minimal synchronous client for HA's `/api/websocket` API.

Used by the "api" adapter (see DESIGN.md, "Deploy path: adapter per
instance") to reach dashboards, area/entity registries, and input_* helpers
— none of these are exposed over REST. Read-only by construction: the only
commands this module ever sends are `auth` and whatever `*_list`/
`lovelace/config` command the caller passes to `HaWsClient.command()` — it
has no method that issues a create/update/delete command.

Depends on the `websocket-client` PyPI package (import name `websocket`),
which is not in the stdlib. See services/home-assistant/README.md,
"WebSocket dependency", for the install command. Importing this module
raises ImportError with an actionable message if it's missing, instead of
the default traceback, so the api adapter can surface one clear line
instead of a stack trace.
"""
import itertools
import json

try:
    import websocket as _ws
except ImportError as exc:  # pragma: no cover - exercised via import.sh error path
    raise ImportError(
        "the 'websocket-client' package is required for the api adapter's "
        "dashboard/registry/helper export (HA's WebSocket API) — install it "
        "with 'sudo apt install python3-websocket' or 'pip install --user "
        "websocket-client' (see services/home-assistant/README.md, "
        "'WebSocket dependency')"
    ) from exc


class HaWsError(RuntimeError):
    """Raised for handshake/auth failures or a command that reports failure."""


class HaWsClient:
    """One `/api/websocket` connection: authenticate once, issue N read-only commands."""

    def __init__(self, base_url, token, timeout=30):
        ws_url = base_url.rstrip("/")
        if ws_url.startswith("https://"):
            ws_url = "wss://" + ws_url[len("https://"):]
        elif ws_url.startswith("http://"):
            ws_url = "ws://" + ws_url[len("http://"):]
        self._url = ws_url + "/api/websocket"
        self._token = token
        self._timeout = timeout
        self._next_id = itertools.count(1)
        self._conn = None

    def __enter__(self):
        self.connect()
        return self

    def __exit__(self, *exc_info):
        self.close()

    def connect(self):
        self._conn = _ws.create_connection(self._url, timeout=self._timeout)
        hello = self._recv()
        if hello.get("type") != "auth_required":
            self.close()
            raise HaWsError(f"unexpected handshake message from {self._url}: {hello!r}")

        self._conn.send(json.dumps({"type": "auth", "access_token": self._token}))
        auth_result = self._recv()
        if auth_result.get("type") != "auth_ok":
            self.close()
            raise HaWsError(
                "websocket auth rejected — check the deploy_agent token "
                f"(server said: {auth_result!r})"
            )

    def close(self):
        if self._conn is not None:
            self._conn.close()
            self._conn = None

    def _recv(self):
        return json.loads(self._conn.recv())

    def command(self, command_type, **kwargs):
        """Send one read-only WS command, return its `result` payload.

        Raises HaWsError if the server reports failure or sends something
        other than a matching `result` message.
        """
        msg_id = next(self._next_id)
        payload = {"id": msg_id, "type": command_type}
        payload.update(kwargs)
        self._conn.send(json.dumps(payload))
        while True:
            msg = self._recv()
            if msg.get("id") != msg_id:
                # Not our response (e.g. an unrelated event) — keep waiting.
                continue
            if msg.get("type") == "result":
                if not msg.get("success", False):
                    raise HaWsError(f"{command_type} failed: {msg.get('error')}")
                return msg.get("result")
            raise HaWsError(f"unexpected response to {command_type}: {msg!r}")
