"""Redis pub/sub for multi-instance domain event fan-out.

- publish_event(): called from Restate services to push events to Redis.
- start_subscriber(): background task that subscribes to Redis and forwards
  events to the in-process ConnectionManager.
- Automatic reconnect with retry on Redis transient failures.
"""

import asyncio

import logfire
import redis.asyncio as aioredis

from app.config import get_settings
from app.realtime.connection_manager import manager
from app.realtime.events import DOMAIN_EVENTS_CHANNEL, DomainEvent

_RECONNECT_DELAY_BASE = 1.0  # seconds
_RECONNECT_DELAY_MAX = 30.0


async def get_redis() -> aioredis.Redis:
    """Create a new async Redis client from settings."""
    settings = get_settings()
    return aioredis.from_url(settings.REDIS_URL, decode_responses=True)


async def publish_event(event: DomainEvent) -> None:
    """Publish a domain event to the Redis pub/sub channel.

    Best-effort: logs and swallows errors so callers are never blocked.
    """
    try:
        client = await get_redis()
        try:
            payload = event.model_dump_json()
            await client.publish(DOMAIN_EVENTS_CHANNEL, payload)
            logfire.info(
                "sse_event_published",
                event_type=event.type,
                entity_id=event.entity_id,
                restaurant_id=event.restaurant_id,
            )
        finally:
            await client.aclose()
    except Exception as exc:
        logfire.warning(
            "sse_event_publish_failed",
            event_type=event.type,
            entity_id=event.entity_id,
            error=str(exc),
        )


async def start_subscriber() -> None:
    """Subscribe to the Redis domain_events channel and forward to ConnectionManager.

    Runs forever with automatic reconnect on failures. Intended to be launched
    as a background task during FastAPI lifespan.
    """
    delay = _RECONNECT_DELAY_BASE

    while True:
        try:
            client = await get_redis()
            pubsub = client.pubsub()
            await pubsub.subscribe(DOMAIN_EVENTS_CHANNEL)
            logfire.info("sse_redis_subscriber_started", channel=DOMAIN_EVENTS_CHANNEL)
            delay = _RECONNECT_DELAY_BASE  # reset on successful connect

            async for message in pubsub.listen():
                if message["type"] != "message":
                    continue
                try:
                    event = DomainEvent.model_validate_json(message["data"])
                    delivered = await manager.enqueue(event)
                    logfire.info(
                        "sse_event_forwarded",
                        event_type=event.type,
                        entity_id=event.entity_id,
                        delivered_to=delivered,
                    )
                except Exception as exc:
                    logfire.warning(
                        "sse_event_forward_error",
                        error=str(exc),
                        raw=str(message.get("data", ""))[:200],
                    )

        except asyncio.CancelledError:
            logfire.info("sse_redis_subscriber_stopped")
            break
        except Exception as exc:
            logfire.warning(
                "sse_redis_subscriber_reconnecting",
                error=str(exc),
                delay=delay,
            )
            await asyncio.sleep(delay)
            delay = min(delay * 2, _RECONNECT_DELAY_MAX)
