"""Busy-session message queue system.

When the agent is busy processing a message for a session, new messages for
that session are handled according to the configured QueueMode:

- **followup**: Queue messages and process them one-by-one after the current
  turn finishes (default).
- **collect**: Collect messages during a debounce window and batch them into
  a single combined prompt.
- **interrupt**: Cancel the current task and process the new message immediately.
- **queue**: Simple FIFO queue (like followup but without debounce).

Adapted from OpenClaw's followup queue system.
"""

from __future__ import annotations

import time
from collections import OrderedDict
from dataclasses import dataclass, field
from enum import Enum
from typing import TYPE_CHECKING

from loguru import logger

if TYPE_CHECKING:
    from asedclaw.bus.events import InboundMessage


# ---------------------------------------------------------------------------
# Enums
# ---------------------------------------------------------------------------

class QueueMode(str, Enum):
    """How to handle new messages when the agent is busy."""

    FOLLOWUP = "followup"
    COLLECT = "collect"
    INTERRUPT = "interrupt"
    QUEUE = "queue"


class QueueDropPolicy(str, Enum):
    """What to do when the queue exceeds its cap."""

    OLD = "old"
    NEW = "new"
    SUMMARIZE = "summarize"


# ---------------------------------------------------------------------------
# Data classes
# ---------------------------------------------------------------------------

@dataclass
class QueuedMessage:
    """A message waiting in the followup queue."""

    msg: InboundMessage
    summary_line: str
    enqueued_at: float = field(default_factory=time.monotonic)


@dataclass
class SessionQueue:
    """Per-session followup queue state."""

    items: list[QueuedMessage] = field(default_factory=list)
    mode: QueueMode = QueueMode.FOLLOWUP
    debounce_ms: int = 1000
    cap: int = 20
    drop_policy: QueueDropPolicy = QueueDropPolicy.SUMMARIZE
    dropped_count: int = 0
    summary_lines: list[str] = field(default_factory=list)


# ---------------------------------------------------------------------------
# Dedup cache
# ---------------------------------------------------------------------------

class _DedupeCache:
    """LRU cache with TTL for message-id deduplication."""

    def __init__(self, max_size: int = 10_000, ttl_s: float = 300.0):
        self._cache: OrderedDict[str, float] = OrderedDict()
        self._max_size = max_size
        self._ttl_s = ttl_s

    def has(self, key: str) -> bool:
        """Check if *key* is in cache and not expired."""
        ts = self._cache.get(key)
        if ts is None:
            return False
        if time.monotonic() - ts > self._ttl_s:
            self._cache.pop(key, None)
            return False
        # Move to end (most-recently-used)
        self._cache.move_to_end(key)
        return True

    def add(self, key: str) -> None:
        """Add *key* to cache."""
        self._cache[key] = time.monotonic()
        self._cache.move_to_end(key)
        self._evict()

    def _evict(self) -> None:
        """Remove oldest entries if over max_size or expired."""
        now = time.monotonic()
        # Evict expired
        while self._cache:
            oldest_key = next(iter(self._cache))
            if now - self._cache[oldest_key] > self._ttl_s:
                self._cache.pop(oldest_key)
            else:
                break
        # Evict by size
        while len(self._cache) > self._max_size:
            self._cache.popitem(last=False)

    def clear(self) -> None:
        self._cache.clear()


# ---------------------------------------------------------------------------
# FollowupQueueManager
# ---------------------------------------------------------------------------

def _summarize(msg: InboundMessage, max_len: int = 100) -> str:
    """Build a short summary line for a message."""
    text = msg.content.strip().replace("\n", " ")
    return (text[:max_len - 1] + "…") if len(text) > max_len else text


class FollowupQueueManager:
    """Manages per-session followup queues with dedup and overflow control."""

    def __init__(self) -> None:
        self._queues: dict[str, SessionQueue] = {}
        self._dedupe = _DedupeCache()

    # -- public API ---------------------------------------------------------

    def enqueue(
        self,
        session_key: str,
        msg: InboundMessage,
        *,
        mode: QueueMode = QueueMode.FOLLOWUP,
        debounce_ms: int = 1000,
        cap: int = 20,
        drop_policy: QueueDropPolicy = QueueDropPolicy.SUMMARIZE,
    ) -> bool:
        """Enqueue a message for later processing.

        Returns ``True`` if the message was accepted, ``False`` if it was
        dropped (duplicate or overflow with NEW policy).
        """
        # Dedup by message_id
        message_id = msg.metadata.get("message_id")
        if message_id:
            dedupe_key = f"{session_key}:{message_id}"
            if self._dedupe.has(dedupe_key):
                logger.debug("Queue dedup: dropping duplicate message_id={}", message_id)
                return False
            self._dedupe.add(dedupe_key)

        queue = self._get_or_create(session_key, mode, debounce_ms, cap, drop_policy)

        # Check content dedup (same content already queued)
        if any(item.msg.content == msg.content for item in queue.items):
            logger.debug("Queue dedup: dropping duplicate content for session={}", session_key)
            return False

        # Overflow handling
        if len(queue.items) >= queue.cap:
            summary = _summarize(msg)
            if queue.drop_policy == QueueDropPolicy.NEW:
                queue.dropped_count += 1
                queue.summary_lines.append(summary)
                logger.info(
                    "Queue overflow (NEW): dropping message for session={} depth={}",
                    session_key, len(queue.items),
                )
                return False
            else:
                # OLD or SUMMARIZE: drop the oldest
                oldest = queue.items.pop(0)
                queue.dropped_count += 1
                queue.summary_lines.append(oldest.summary_line)
                logger.info(
                    "Queue overflow ({}): dropped oldest for session={} depth={}",
                    queue.drop_policy.value, session_key, len(queue.items),
                )

        queued = QueuedMessage(msg=msg, summary_line=_summarize(msg))
        queue.items.append(queued)
        logger.debug(
            "Enqueued message for session={} depth={}",
            session_key, len(queue.items),
        )
        return True

    def drain_next(self, session_key: str) -> QueuedMessage | None:
        """Pop the next queued message for sequential processing."""
        queue = self._queues.get(session_key)
        if not queue or not queue.items:
            return None
        return queue.items.pop(0)

    def drain_all_collected(self, session_key: str) -> list[QueuedMessage]:
        """Pop all queued messages for batch (collect mode) processing."""
        queue = self._queues.get(session_key)
        if not queue or not queue.items:
            return []
        items = list(queue.items)
        queue.items.clear()
        return items

    def get_dropped_summary(self, session_key: str) -> list[str]:
        """Return and clear dropped message summaries."""
        queue = self._queues.get(session_key)
        if not queue:
            return []
        lines = list(queue.summary_lines)
        dropped = queue.dropped_count
        queue.summary_lines.clear()
        queue.dropped_count = 0
        if dropped > 0:
            logger.info("Queue: {} messages were dropped for session={}", dropped, session_key)
        return lines

    def clear(self, session_key: str) -> int:
        """Clear the queue for a session.  Returns count of cleared items."""
        queue = self._queues.pop(session_key, None)
        if not queue:
            return 0
        count = len(queue.items) + queue.dropped_count
        logger.info("Queue cleared for session={} items={}", session_key, count)
        return count

    def get_depth(self, session_key: str) -> int:
        """Current number of queued messages."""
        queue = self._queues.get(session_key)
        return len(queue.items) if queue else 0

    def get_debounce_ms(self, session_key: str) -> int:
        """Debounce window for the session's queue."""
        queue = self._queues.get(session_key)
        return queue.debounce_ms if queue else 1000

    def get_mode(self, session_key: str) -> QueueMode | None:
        """Queue mode for the session, or None if no queue exists."""
        queue = self._queues.get(session_key)
        return queue.mode if queue else None

    # -- internals ----------------------------------------------------------

    def _get_or_create(
        self,
        session_key: str,
        mode: QueueMode,
        debounce_ms: int,
        cap: int,
        drop_policy: QueueDropPolicy,
    ) -> SessionQueue:
        queue = self._queues.get(session_key)
        if queue is not None:
            # Update settings (may change between messages via directive)
            queue.mode = mode
            queue.debounce_ms = debounce_ms
            queue.cap = cap
            queue.drop_policy = drop_policy
            return queue
        queue = SessionQueue(
            mode=mode,
            debounce_ms=debounce_ms,
            cap=cap,
            drop_policy=drop_policy,
        )
        self._queues[session_key] = queue
        return queue
