"""Daemon event inbox — Redis Stream-based producer/consumer with consumer groups.

Producer: daemon writes structured events via publish_daemon_event().
Consumer: delegator/dashboard reads via drain_events() with XREADGROUP + XACK.
"""

from __future__ import annotations

import json
from datetime import datetime, timezone

from pydantic import BaseModel, Field
from redis.asyncio import Redis

from loom.bus import channels


class DaemonEvent(BaseModel):
    """A structured event produced by the daemon."""

    event_type: str
    project_id: str
    task_id: str | None = None
    details: dict = Field(default_factory=dict)
    timestamp: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())

    def to_stream_dict(self) -> dict[str, str]:
        """Serialize to flat string dict for Redis XADD."""
        return {
            "event_type": self.event_type,
            "project_id": self.project_id,
            "task_id": self.task_id or "",
            "details": json.dumps(self.details),
            "timestamp": self.timestamp,
        }

    @classmethod
    def from_stream_dict(cls, data: dict[str, str]) -> DaemonEvent:
        """Deserialize from Redis stream entry."""
        return cls(
            event_type=data["event_type"],
            project_id=data["project_id"],
            task_id=data["task_id"] or None,
            details=json.loads(data.get("details", "{}")),
            timestamp=data.get("timestamp", ""),
        )


async def publish_daemon_event(
    redis: Redis,
    project_id: str,
    event_type: str,
    task_id: str | None = None,
    details: dict | None = None,
) -> str:
    """Publish a daemon event to the project's inbox stream. Returns stream ID."""
    evt = DaemonEvent(
        event_type=event_type,
        project_id=project_id,
        task_id=task_id,
        details=details or {},
    )
    key = channels.daemon_inbox(project_id)
    return await redis.xadd(key, evt.to_stream_dict())


async def create_consumer_group(
    redis: Redis,
    project_id: str,
    group_name: str,
) -> None:
    """Create a consumer group on the daemon inbox stream.

    Idempotent — safe to call multiple times. New groups start reading
    from the latest message ($), so they only see events published after creation.
    """
    key = channels.daemon_inbox(project_id)
    try:
        await redis.xgroup_create(key, group_name, id="0", mkstream=True)
    except Exception as e:
        # BUSYGROUP = group already exists
        if "BUSYGROUP" in str(e):
            return
        raise


async def drain_events(
    redis: Redis,
    project_id: str,
    group_name: str,
    consumer_id: str,
    count: int = 100,
) -> list[DaemonEvent]:
    """Read and acknowledge pending events from the inbox stream.

    Returns a list of DaemonEvent objects. Events are ACKed immediately
    so they won't be re-delivered to this consumer group.
    """
    key = channels.daemon_inbox(project_id)
    # XREADGROUP: ">" means undelivered messages only
    result = await redis.xreadgroup(
        group_name, consumer_id, {key: ">"}, count=count,
    )
    if not result:
        return []

    events: list[DaemonEvent] = []
    # result format: [[stream_key, [(id, data), ...]]]
    for _stream_key, entries in result:
        for entry_id, data in entries:
            events.append(DaemonEvent.from_stream_dict(data))
            await redis.xack(key, group_name, entry_id)

    return events
