"""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"

_MAX_DRAIN = 32


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
        self._retry_after_mono = 0.0

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

    def _ensure(self) -> redis.Redis | None:
        now = time.monotonic()
        if now < self._retry_after_mono:
            return None
        try:
            client = self._connect()
            client.ping()
            self._backoff_s = 1.0
            self._retry_after_mono = 0.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
            self._retry_after_mono = now + 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

    @staticmethod
    def _append_message(out: list[tuple[str, str]], msg: dict[str, Any] | None) -> bool:
        if msg is None:
            return False
        if msg.get("type") != "message":
            return False
        channel = msg.get("channel")
        data = msg.get("data")
        if isinstance(channel, str) and isinstance(data, str):
            out.append((channel, data))
            return True
        return False

    def wait_messages(self, timeout_s: float) -> list[tuple[str, str]]:
        """Block until the first message or timeout, then drain queued messages."""
        pubsub = self.subscribe_commands_and_temps()
        if pubsub is None:
            return []
        out: list[tuple[str, str]] = []
        first = pubsub.get_message(timeout=max(0.0, timeout_s))
        self._append_message(out, first)
        while len(out) < _MAX_DRAIN:
            msg = pubsub.get_message(timeout=0)
            if msg is None:
                break
            if not self._append_message(out, msg):
                continue
        return out

    def publish_state(self, payload: dict[str, Any]) -> bool:
        client = self._ensure()
        if client is None:
            return False
        try:
            body = json.dumps(payload)
            client.set(KEY_CONTROL_LATEST, body)
            client.publish(CHAN_CONTROL_STATE, body)
            return True
        except Exception as exc:
            logger.warning("publish state failed: %s", exc)
            self._client = None
            self._pubsub = None
            return False

    def publish_health(self, payload: dict[str, Any]) -> bool:
        client = self._ensure()
        if client is None:
            return False
        try:
            body = json.dumps(payload)
            client.set(KEY_HEALTH, body)
            client.publish(CHAN_HEALTH, body)
            return True
        except Exception as exc:
            logger.warning("publish health failed: %s", exc)
            self._client = None
            self._pubsub = None
            return False

    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
