#!/usr/bin/env python3
"""Feeds the eww weather-overlay widget one JSON object per change on
`smarthome/weather/current`. Installed to /usr/local/bin/weather-json, consumed
by eww's `deflisten` (see configs/eww/eww.yuck).

Session-scoped and independent of thinclient_agent — same reasoning as
now-playing-json: this dies with sway, thinclient_agent is a system service that
must survive it, and wiring the two together would mean either a second inbound
control channel into the agent (which mqtt_discovery.py's security boundary
rules out) or restarting the agent whenever this widget restarts. MQTT
credentials are just re-read from the same config.env thinclient_agent itself
reads; this script never publishes anything and never touches that file.

DATA CONTRACT: `smarthome/weather/current`, retained, a small JSON object
`{"temperature": "...", "condition": "...", "location": "..."}` — published by a
Home Assistant automation this repo does not build, see
hosts/thin-client/README.md's "Idle-gallery weather overlay" section for the
worked example. Household-wide, not per-thin-client, same reasoning as
thinclient_agent/main.py's DIGEST_VIEWED_TOPIC: weather is one household-wide
fact, not something scoped to whichever room happens to be asking.

Degrades to {"available": false} — the clock/date still show, only the weather
line disappears, see eww.yuck — whenever MQTT is unreachable or nothing has ever
been published to the topic yet. Never blocks the rest of the widget on it.
"""

from __future__ import annotations

import json
import os
import sys

import paho.mqtt.client as mqtt

CONFIG_PATH = os.environ.get("THINCLIENT_AGENT_CONFIG", "/etc/thinclient-agent/config.env")
TOPIC = "smarthome/weather/current"


def load_config(path: str) -> dict[str, str]:
    values: dict[str, str] = {}
    try:
        with open(path, encoding="utf-8") as handle:
            for line in handle:
                line = line.strip()
                if not line or line.startswith("#") or "=" not in line:
                    continue
                key, _, value = line.partition("=")
                values[key.strip()] = value.strip().strip('"').strip("'")
    except OSError:
        pass
    return values


def emit(state: dict) -> None:
    sys.stdout.write(json.dumps(state) + "\n")
    sys.stdout.flush()


def unavailable() -> dict:
    return {"available": False, "temperature": "", "condition": "", "location": ""}


def parse(payload: bytes) -> dict:
    try:
        data = json.loads(payload.decode("utf-8", "replace"))
    except ValueError:
        return unavailable()
    if not isinstance(data, dict):
        return unavailable()
    return {
        "available": True,
        "temperature": str(data.get("temperature") or ""),
        "condition": str(data.get("condition") or ""),
        "location": str(data.get("location") or ""),
    }


def make_client() -> mqtt.Client:
    callback_api = getattr(mqtt, "CallbackAPIVersion", None)
    if callback_api is not None:
        return mqtt.Client(callback_api.VERSION1, client_id="weather-json")
    return mqtt.Client(client_id="weather-json")


def main() -> int:
    config = load_config(CONFIG_PATH)
    broker_host = config.get("MQTT_BROKER_HOST", "")
    if not broker_host:
        # No broker configured at all — emit once and idle rather than spin
        # retrying a connection that was never going to happen.
        emit(unavailable())
        return 0

    broker_port = int(config.get("MQTT_BROKER_PORT") or 1883)
    client = make_client()
    if config.get("MQTT_USERNAME"):
        client.username_pw_set(config["MQTT_USERNAME"], config.get("MQTT_PASSWORD") or None)

    def on_connect(_client, _userdata, _flags, rc):
        if rc == 0:
            client.subscribe(TOPIC, qos=1)
        else:
            emit(unavailable())

    def on_disconnect(_client, _userdata, rc):
        emit(unavailable())

    def on_message(_client, _userdata, message):
        emit(parse(message.payload))

    client.on_connect = on_connect
    client.on_disconnect = on_disconnect
    client.on_message = on_message

    emit(unavailable())
    # connect_async, never a blocking connect(): a thin client with the container
    # host powered off must still show its clock, same "never stall the session
    # on a remote service" rule Phase 11.10 applies everywhere else on this image.
    client.connect_async(broker_host, broker_port, keepalive=60)
    client.loop_forever()
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
