"""Publish temperature readings, setup, and health to Redis."""

from __future__ import annotations

import json
import logging
import time
from typing import Any

try:
    import redis
except ImportError:
    redis = None  # type: ignore[assignment]

logger = logging.getLogger("hot_tub.temp")

KEY_LATEST = "temps:latest"
CHAN_READING = "temps.reading"
KEY_SETUP = "temps:setup"
CHAN_SETUP = "temps.setup"
KEY_HEALTH = "health:temps"
CHAN_HEALTH = "health.temps"
CHAN_COMMAND = "temps.command"


class RedisPublisher:
    def __init__(self, redis_url: str) -> None:
        self._url = redis_url
        self._client: redis.Redis | None = None
        self._pubsub: redis.client.PubSub | None = None
        self._last_setup_json: str | None = None
        self._backoff_s = 1.0
        self._missing_redis_logged = False

    def _connect(self) -> redis.Redis:
        if self._client is None:
            self._client = redis.from_url(self._url, decode_responses=True)
        return self._client

    def _ensure(self) -> redis.Redis | None:
        if redis is None:
            if not self._missing_redis_logged:
                logger.error(
                    "redis Python package not installed; install with: sudo apt install python3-redis"
                )
                self._missing_redis_logged = True
            return None
        try:
            client = self._connect()
            client.ping()
            self._backoff_s = 1.0
            return client
        except Exception as exc:
            logger.warning("Redis unavailable: %s (retry in %.0fs)", exc, self._backoff_s)
            self._pubsub = None
            self._client = None
            time.sleep(min(self._backoff_s, 30.0))
            self._backoff_s = min(self._backoff_s * 2, 30.0)
            return None

    def subscribe_commands(self) -> redis.client.PubSub | None:
        client = self._ensure()
        if client is None:
            return None
        try:
            if self._pubsub is None:
                self._pubsub = client.pubsub(ignore_subscribe_messages=True)
                self._pubsub.subscribe(CHAN_COMMAND)
            return self._pubsub
        except Exception as exc:
            logger.warning("Redis subscribe failed: %s", exc)
            self._pubsub = None
            self._client = None
            return None

    def poll_commands(self, timeout_s: float = 0.5) -> list[str]:
        pubsub = self.subscribe_commands()
        if pubsub is None:
            return []
        out: list[str] = []
        deadline = time.monotonic() + timeout_s
        while time.monotonic() < deadline:
            msg = pubsub.get_message(timeout=0.1)
            if msg is None:
                continue
            if msg.get("type") != "message":
                continue
            data = msg.get("data")
            if isinstance(data, str):
                out.append(data)
            if len(out) >= 32:
                break
        return out

    def publish_reading(self, payload: dict[str, Any]) -> bool:
        client = self._ensure()
        if client is None:
            return False
        body = json.dumps(payload)
        client.set(KEY_LATEST, body)
        client.publish(CHAN_READING, body)
        return True

    def publish_setup(self, payload: dict[str, Any], *, force: bool = False) -> bool:
        client = self._ensure()
        if client is None:
            return False
        body = json.dumps(payload)
        if not force and body == self._last_setup_json:
            return True
        self._last_setup_json = body
        client.set(KEY_SETUP, body)
        client.publish(CHAN_SETUP, body)
        return True

    def publish_health(self, payload: dict[str, Any]) -> bool:
        client = self._ensure()
        if client is None:
            return False
        body = json.dumps(payload)
        client.set(KEY_HEALTH, body)
        client.publish(CHAN_HEALTH, body)
        return True

    def close(self) -> None:
        if self._pubsub is not None:
            try:
                self._pubsub.unsubscribe()
                self._pubsub.close()
            except Exception:
                pass
            self._pubsub = None
        if self._client is not None:
            try:
                self._client.close()
            except Exception:
                pass
            self._client = None
