"""Home Assistant MQTT Discovery payloads and command dispatch.

SECURITY BOUNDARY — this module is the entire remote-control API of the Steam TV box.

Same principle as hosts/thin-client/agent/thinclient_agent/mqtt_discovery.py and the
touch panel's, applied to this device: the local LLM never gets a network path to this
machine. The only chain is: LLM tool call -> Home Assistant service call -> MQTT ->
this dispatcher. That property holds only as long as this stays the sole inbound
control surface: no HTTP listener, no websocket server, no exposed Sway IPC socket, no
shell endpoint. A new feature belongs as another entity below, not as another listener.

It is worth restating on this host in particular, because it is the one machine here
with a real GPU, a Steam login and a games library on it — i.e. the one with something
worth taking. Nothing in this image listens on a port except sshd (key-only) and wayvnc
(password-mandatory, fails closed). Steam's own remote-play ports are not opened by
anything here; if you want them, that is a deliberate decision to make in the firewall,
not a default this image ships.

Every command handler here is a fixed, enumerated action. A payload never becomes an
argv element, a shell string, or a URL host — see the launch table in main.py, which
builds every command from local constants and uses the payload only to pick between
known values.
"""

from __future__ import annotations

import json
import logging
from typing import Callable

from . import __version__

log = logging.getLogger(__name__)

DISCOVERY_PREFIX = "homeassistant"


class Discovery:
    def __init__(self, client, node_id: str, friendly_name: str, room: str = ""):
        self.client = client
        self.node_id = node_id
        self.friendly_name = friendly_name
        self.room = (room or "").strip()
        self.base = f"steamtv/{node_id}"
        self.availability_topic = f"{self.base}/availability"
        self.media_state_topic = f"{self.base}/media/state"
        self.session_state_topic = f"{self.base}/session/state"
        self._handlers: dict[str, Callable[[str], None]] = {}

        self.device = {
            "identifiers": [f"steamtv_{node_id}"],
            "name": friendly_name,
            "manufacturer": "SmartestHome",
            "model": "Steam TV box",
            "sw_version": __version__,
        }
        # Which room this physically sits in, as an HA area_id. `suggested_area` is
        # honoured by HA only when the device is FIRST discovered — moving a device
        # later means moving it in HA too, this cannot un-file it. Omitted entirely
        # when unset, because an empty suggested_area is not the same request as no
        # suggestion. See docs/rooms-and-endpoints.md.
        if self.room:
            self.device["suggested_area"] = self.room

    # --- plumbing -----------------------------------------------------------
    def _publish_config(self, component: str, object_id: str, payload: dict) -> None:
        payload = {
            "availability_topic": self.availability_topic,
            "device": self.device,
            "unique_id": f"{self.node_id}_{object_id}",
            **payload,
        }
        topic = f"{DISCOVERY_PREFIX}/{component}/{self.node_id}/{object_id}/config"
        self.client.publish(topic, json.dumps(payload), qos=1, retain=True)

    def _command_topic(self, suffix: str, handler) -> str:
        topic = f"{self.base}/{suffix}"
        self._handlers[topic] = handler
        return topic

    def subscribe_all(self) -> None:
        for topic in self._handlers:
            self.client.subscribe(topic, qos=1)

    def dispatch(self, topic: str, payload: str) -> None:
        handler = self._handlers.get(topic)
        if handler is None:
            log.warning("no handler for %s", topic)
            return
        try:
            handler(payload)
        except Exception:
            log.exception("handler for %s failed", topic)

    def publish_available(self, available: bool = True) -> None:
        self.client.publish(
            self.availability_topic,
            "online" if available else "offline",
            qos=1,
            retain=True,
        )

    def publish_media_state(self, state: dict) -> None:
        self.client.publish(self.media_state_topic, json.dumps(state), qos=0, retain=True)

    # --- media --------------------------------------------------------------
    def register_media_player(self, on_command, on_volume) -> None:
        command_topic = self._command_topic("media/command", on_command)
        volume_topic = self._command_topic("media/volume/set", lambda p: on_volume(float(p)))

        # Core Home Assistant's MQTT integration has NO media_player platform — see
        # hosts/thin-client/README.md's identical caveat. The button/number entities
        # below give the same transport control with stock HA.
        self._publish_config(
            "media_player",
            "media",
            {
                "name": "Media",
                "state_topic": self.media_state_topic,
                "state_template": "{{ value_json.state }}",
                "command_topic": command_topic,
                "volume_command_topic": volume_topic,
                "volume_state_topic": self.media_state_topic,
                "volume_template": "{{ value_json.volume }}",
                "title_template": "{{ value_json.title }}",
                "artist_template": "{{ value_json.artist }}",
                "album_template": "{{ value_json.album }}",
            },
        )

        for object_id, name, payload, icon in (
            ("media_play_pause", "Play/pause", "PLAY_PAUSE", "mdi:play-pause"),
            ("media_next", "Next track", "NEXT", "mdi:skip-next"),
            ("media_previous", "Previous track", "PREVIOUS", "mdi:skip-previous"),
            ("media_stop", "Stop", "STOP", "mdi:stop"),
        ):
            self._publish_config(
                "button",
                object_id,
                {
                    "name": name,
                    "command_topic": command_topic,
                    "payload_press": payload,
                    "icon": icon,
                },
            )

        self._publish_config(
            "sensor",
            "media_state",
            {
                "name": "Playback state",
                "state_topic": self.media_state_topic,
                "value_template": "{{ value_json.state }}",
                "json_attributes_topic": self.media_state_topic,
                "icon": "mdi:play-circle",
            },
        )

        self._publish_config(
            "number",
            "media_volume",
            {
                "name": "Volume",
                "command_topic": volume_topic,
                "state_topic": self.media_state_topic,
                "value_template": "{{ value_json.volume }}",
                "min": 0,
                "max": 1,
                "step": 0.05,
                "mode": "slider",
                "icon": "mdi:volume-high",
            },
        )

    # --- apps / workspaces --------------------------------------------------
    def register_app_launchers(self, apps, on_launch) -> None:
        for key, app in apps.items():
            self._publish_config(
                "button",
                f"launch_{key}",
                {
                    "name": f"Launch {app.name}",
                    "command_topic": self._command_topic(
                        f"app/{key}/launch",
                        lambda _payload, key=key: on_launch(key),
                    ),
                    "icon": app.icon,
                },
            )

    def register_workspace_select(self, workspaces, on_workspace, state_topic_value) -> None:
        self._publish_config(
            "select",
            "workspace",
            {
                "name": "Screen",
                "command_topic": self._command_topic("workspace/set", on_workspace),
                "state_topic": f"{self.base}/workspace/state",
                "options": list(workspaces),
                "icon": "mdi:view-dashboard",
            },
        )
        self.client.publish(
            f"{self.base}/workspace/state", state_topic_value, qos=1, retain=True
        )

    def publish_workspace(self, name: str) -> None:
        self.client.publish(f"{self.base}/workspace/state", name, qos=1, retain=True)

    # --- session mode -------------------------------------------------------
    def register_session_mode(self, modes, on_select, on_stop_media) -> None:
        """The "what is this box doing" surface — see session_mode.py.

        A sensor and a select rather than one entity, because the two are not the same
        question. The sensor reports four states (gaming / steam / media / idle); the
        select offers only the two that are meaningful to *ask for*. "idle" is not
        something you can request, and "steam" (Steam up but not focused) is a
        transitional state nobody sets on purpose.
        """
        self._publish_config(
            "sensor",
            "session_mode",
            {
                "name": "Session",
                "state_topic": self.session_state_topic,
                "value_template": "{{ value_json.mode }}",
                "json_attributes_topic": self.session_state_topic,
                "icon": "mdi:gamepad-variant",
            },
        )

        self._publish_config(
            "select",
            "session_mode_select",
            {
                "name": "Mode",
                "command_topic": self._command_topic("session/mode/set", on_select),
                "state_topic": self.session_state_topic,
                "value_template": "{{ value_json.mode }}",
                "options": list(modes),
                "icon": "mdi:gamepad-variant",
            },
        )

        # The only thing in this whole surface that shuts something down, and therefore
        # its own explicit button rather than a side effect of switching mode. See
        # SessionMode.select()'s docstring for why the mode switch is additive.
        self._publish_config(
            "button",
            "stop_media",
            {
                "name": "Stop media apps",
                "command_topic": self._command_topic(
                    "session/media/stop", lambda _payload: on_stop_media()
                ),
                "icon": "mdi:close-circle-outline",
            },
        )

    def publish_session_state(self, mode: str, attributes: dict | None = None) -> None:
        payload = {"mode": mode}
        if attributes:
            payload.update(attributes)
        self.client.publish(
            self.session_state_topic, json.dumps(payload), qos=1, retain=True
        )

    # --- audio output -------------------------------------------------------
    def register_audio_output(self, options, current, on_select) -> None:
        self._publish_config(
            "select",
            "audio_output",
            {
                "name": "Audio output",
                "command_topic": self._command_topic("audio/output/set", on_select),
                "state_topic": f"{self.base}/audio/output/state",
                "options": list(options),
                "icon": "mdi:speaker",
            },
        )
        self.publish_audio_output(current)

    def publish_audio_output(self, option: str) -> None:
        self.client.publish(
            f"{self.base}/audio/output/state", option, qos=1, retain=True
        )

    # --- display power ------------------------------------------------------
    def register_display_power(self, on_command, initial: bool = True) -> None:
        self._publish_config(
            "switch",
            "display",
            {
                "name": "Display",
                "command_topic": self._command_topic("display/set", on_command),
                "state_topic": f"{self.base}/display/state",
                "payload_on": "ON",
                "payload_off": "OFF",
                "icon": "mdi:television",
            },
        )
        self.publish_display_power(initial)

    def publish_display_power(self, on: bool) -> None:
        self.client.publish(
            f"{self.base}/display/state", "ON" if on else "OFF", qos=1, retain=True
        )
