"""Config message listener: wait_for_ack, wait_for_config, poll."""
from __future__ import annotations

import logging
import time
from typing import Any, Dict, List, Optional

logger = logging.getLogger(__name__)


class ConfigListener:
    """Listens to the results stream for registration acks and tasks stream for config updates."""

    def __init__(self, conn, results_stream: str, tasks_stream: str, instance_id: str) -> None:
        self._conn = conn          # RedisConnection
        self._results_stream = results_stream
        self._tasks_stream = tasks_stream
        self._instance_id = instance_id
        self.last_config_id: Optional[str] = None
        self.last_registration_id: Optional[str] = None
        # Cursor for the tasks stream so config polling does not re-read the same
        # non-config entries on every loop.
        self.last_tasks_id: Optional[str] = None

    def _read_recent_entries(self, stream: str, count: int = 50) -> List[Any]:
        client = self._conn.client
        if not hasattr(client, "xrevrange"):
            return []
        try:
            return client.xrevrange(stream, "+", "-", count=count)
        except TypeError:
            # Some test doubles expose a simplified signature.
            return client.xrevrange(stream, count=count)
        except Exception:
            return []

    def _latest_existing_config(self) -> Optional[Dict[str, Any]]:
        from actor.runtime.config_store import parse_config_payload

        recent = self._read_recent_entries(self._tasks_stream, count=100)
        for msg in recent:
            if not isinstance(msg, tuple) or len(msg) < 2:
                continue
            msg_id, msg_data = msg
            if isinstance(msg_data, dict) and msg_data.get("type") == "config":
                self.last_config_id = msg_id
                self.last_tasks_id = msg_id
                logger.info("Found existing config message id=%s on tasks stream", msg_id)
                return parse_config_payload(msg_data)
        return None

    # -- blocking waits --------------------------------------------------

    def capture_results_cursor(self) -> str:
        """Snapshot the latest results stream ID so a subsequent wait doesn't miss messages.

        Call this *before* publishing a message that will trigger a response
        on the results stream (e.g. instance_register).
        """
        try:
            info = self._conn.client.xinfo_stream(self._results_stream)
            last = info.get("last-generated-id") or info.get("last_generated_id")
            if last:
                return last
        except Exception:
            pass
        return "$"

    def wait_for_registration_ack(
        self,
        instance_id: str,
        timeout: int = 30,
        accept_config_as_ack: bool = True,
        start_id: Optional[str] = None,
    ) -> Optional[Dict[str, Any]]:
        start = time.time()
        last_id = start_id or "$"
        while time.time() - start < timeout:
            messages = self._conn.client.xread({self._results_stream: last_id}, count=10, block=1000)
            for _, msg_list in messages:
                for msg_id, msg_data in msg_list:
                    last_id = msg_id
                    msg_type = msg_data.get("type")
                    if msg_type == "instance_register_ack" and msg_data.get("instance_id") == instance_id:
                        logger.info("Received instance_register_ack id=%s", msg_id)
                        self.last_registration_id = msg_id
                        return msg_data
                    if accept_config_as_ack and msg_type == "config":
                        logger.info("Treating config message id=%s as registration ack", msg_id)
                        self.last_config_id = msg_id
                        return msg_data
        logger.warning("Registration ack not received within %ss; proceeding", timeout)
        return None

    def wait_for_config(self, timeout: int = 30) -> Dict[str, Any]:
        from actor.runtime.config_store import parse_config_payload

        # Startup race protection: config may already be in the stream before we
        # begin blocking reads.
        if self.last_config_id is None:
            existing = self._latest_existing_config()
            if existing is not None:
                return existing

        start = time.time()
        last_id = self.last_tasks_id or self.last_config_id or "$"
        while time.time() - start < timeout:
            messages = self._conn.client.xread({self._tasks_stream: last_id}, count=10, block=1000)
            for _, msg_list in messages:
                for msg_id, msg_data in msg_list:
                    self.last_tasks_id = msg_id
                    if msg_data.get("type") == "config":
                        logger.info("Received config message id=%s", msg_id)
                        self.last_config_id = msg_id
                        return parse_config_payload(msg_data)
                    last_id = msg_id
        logger.error("Timed out waiting for config after %ss", timeout)
        raise TimeoutError(f"Config not received within {timeout}s")

    def poll_config_updates(self, block_ms: int = 1000, count: int = 5) -> List[Dict[str, Any]]:
        from actor.runtime.config_store import parse_config_payload

        # If we never observed a config cursor, use the latest existing config to
        # avoid missing an already-enqueued update.
        if self.last_config_id is None:
            existing = self._latest_existing_config()
            if existing is not None:
                return [existing]

        last_id = self.last_tasks_id or self.last_config_id or "$"
        configs: List[Dict[str, Any]] = []
        messages = self._conn.client.xread({self._tasks_stream: last_id}, count=count, block=block_ms)
        for _, msg_list in messages:
            for msg_id, msg_data in msg_list:
                last_id = msg_id
                self.last_tasks_id = msg_id
                if msg_data.get("type") == "config":
                    if self.last_config_id == msg_id:
                        continue
                    self.last_config_id = msg_id
                    configs.append(parse_config_payload(msg_data))
        return configs
