"""Bridges mpv's and Spotify's MPRIS state to the HA media_player entity via playerctl.

Identical in shape and reasoning to
hosts/thin-client/agent/thinclient_agent/mpris_bridge.py — see that file's docstring
for why this polls via subprocess rather than holding a dbus connection open.

PLAYER_PRIORITY lists mpv first, then the Spotify GUI client. That order is the answer
to "both are alive, which one does the remote's play button reach?", and mpv wins
because it is the one holding something somebody deliberately opened.

What is NOT in that list is any game. Steam exposes no MPRIS bus, and a game's audio is
not something you "pause" from a phone — the volume controls in audio_control.py are
the right surface for a game making noise, not these transport buttons.

This bridge reports "off" for most of a gaming session, and that is correct rather than
broken: on this image the media players genuinely do not exist until somebody leaves
Big Picture (see configs/session/media-session). The entity to watch for "what is this
box doing" is the session-mode sensor in session_mode.py.
"""

from __future__ import annotations

import logging
import subprocess
import threading

log = logging.getLogger(__name__)

PLAYER_PRIORITY = "mpv,spotify,%any"

_STATUS_TO_HA = {
    "Playing": "playing",
    "Paused": "paused",
    "Stopped": "idle",
}

_METADATA_FORMAT = "{{title}}\x1f{{artist}}\x1f{{album}}\x1f{{mpris:length}}\x1f{{mpris:artUrl}}"


class MprisBridge:
    def __init__(self, publish_state, env_provider, poll_interval: float = 2.0):
        self._publish_state = publish_state
        # playerctl needs DBUS_SESSION_BUS_ADDRESS, which this system service does not
        # inherit; SwayControl.session_env() derives it from the kiosk user's runtime dir.
        self._env_provider = env_provider
        self._poll_interval = poll_interval
        self._last_state: dict | None = None

    def _playerctl(self, *args: str) -> str | None:
        try:
            result = subprocess.run(
                ["playerctl", "-p", PLAYER_PRIORITY, *args],
                env=self._env_provider(),
                capture_output=True,
                text=True,
                timeout=5,
                check=False,
            )
        except (OSError, subprocess.TimeoutExpired) as exc:
            log.debug("playerctl %s failed: %s", " ".join(args), exc)
            return None
        if result.returncode != 0:
            return None
        return result.stdout.strip()

    def read_state(self) -> dict:
        status = self._playerctl("status")
        if status is None:
            return {"state": "off"}

        state = {"state": _STATUS_TO_HA.get(status, "idle")}

        metadata = self._playerctl("metadata", "--format", _METADATA_FORMAT)
        if metadata:
            title, artist, album, length, art_url = (metadata.split("\x1f") + [""] * 5)[:5]
            state["title"] = title
            state["artist"] = artist
            state["album"] = album
            state["art_url"] = art_url
            if length.isdigit():
                state["duration"] = int(length) // 1_000_000

        position = self._playerctl("position")
        if position:
            try:
                state["position"] = int(float(position))
            except ValueError:
                pass

        volume = self._playerctl("volume")
        if volume:
            try:
                state["volume"] = round(float(volume), 3)
            except ValueError:
                pass

        return state

    def poll_once(self) -> None:
        state = self.read_state()
        if state != self._last_state:
            self._last_state = state
            self._publish_state(state)

    def run_forever(self, stop_event: threading.Event) -> None:
        while not stop_event.is_set():
            try:
                self.poll_once()
            except Exception:
                log.exception("MPRIS poll failed")
            stop_event.wait(self._poll_interval)

    # --- command side -------------------------------------------------------
    def handle_command(self, command: str) -> None:
        command = command.strip().upper()
        action = {
            "PLAY": ("play",),
            "PAUSE": ("pause",),
            "PLAY_PAUSE": ("play-pause",),
            "TOGGLE": ("play-pause",),
            "STOP": ("stop",),
            "NEXT": ("next",),
            "PREVIOUS": ("previous",),
            "PREV": ("previous",),
        }.get(command)

        if action is None:
            log.warning("ignoring unknown media command %r", command)
            return

        log.info("media command %s", command)
        self._playerctl(*action)
        self.poll_once()

    def set_volume(self, level: float) -> None:
        level = max(0.0, min(1.0, level))
        self._playerctl("volume", f"{level:.3f}")
        self.poll_once()
