"""Redis subscribe/publish for control daemon."""

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.control")

KEY_CONTROL_LATEST = "control:latest"
CHAN_CONTROL_STATE = "control.state"
KEY_HEALTH = "health:control"
CHAN_HEALTH = "health.control"
KEY_TEMPS_LATEST = "temps:latest"
CHAN_COMMAND = "control.command"
CHAN_TEMPS_READING = "temps.reading"


class ControlRedis:
    def __init__(self, redis_url: str) -> None:
        if redis is None:
            raise RuntimeError("redis package not installed (apt: python3-redis)")
        self._url = redis_url
        self._client: redis.Redis | None = None
        self._pubsub: redis.client.PubSub | None = None
        self._backoff_s = 1.0

    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:
        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._client = None
            self._pubsub = None
            time.sleep(min(self._backoff_s, 30.0))
            self._backoff_s = min(self._backoff_s * 2, 30.0)
            return None

    def get_temps_latest(self) -> dict[str, Any] | None:
        client = self._ensure()
        if client is None:
            return None
        try:
            raw = client.get(KEY_TEMPS_LATEST)
            if not raw:
                return None
            data = json.loads(raw)
            return data if isinstance(data, dict) else None
        except Exception as exc:
            logger.warning("GET temps:latest failed: %s", exc)
            return None

    def subscribe_commands_and_temps(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, CHAN_TEMPS_READING)
            return self._pubsub
        except Exception as exc:
            logger.warning("Redis subscribe failed: %s", exc)
            self._pubsub = None
            self._client = None
            return None

    def poll_messages(self, timeout_s: float = 0.5) -> list[tuple[str, str]]:
        pubsub = self.subscribe_commands_and_temps()
        if pubsub is None:
            return []
        out: list[tuple[str, 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
            channel = msg.get("channel")
            data = msg.get("data")
            if isinstance(channel, str) and isinstance(data, str):
                out.append((channel, data))
            if len(out) >= 32:
                break
        return out

    def publish_state(self, payload: dict[str, Any]) -> bool:
        client = self._ensure()
        if client is None:
            return False
        body = json.dumps(payload)
        client.set(KEY_CONTROL_LATEST, body)
        client.publish(CHAN_CONTROL_STATE, 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
