"""
Deduplication and Recovery Semantics
=====================================
1. IN-MEMORY DEDUP: ``_seen_ids`` tracks the last 1 000 message IDs (session-only).
2. PERSISTED LAST_ACK: After each ACK, the message ID is saved to a Redis hash.
   On restart, messages with ID <= last_ack are skipped.
3. AT-LEAST-ONCE: If the agent crashes between execution and ACK the message is
   redelivered.  Callbacks MUST be idempotent.
4. PENDING RECOVERY: On idle, the consumer claims messages pending > 5 min.
5. RESTART: The consumer reads ``">"`` (new only); pending recovery handles un-ACKed.
"""
from __future__ import annotations

import logging
import threading
from collections import OrderedDict
from typing import Any, Dict, List, Optional

logger = logging.getLogger(__name__)


class DeduplicationTracker:
    """Thread-safe in-memory dedup + persisted last_ack for at-least-once delivery."""

    def __init__(self, conn, state_key: str, max_seen: int = 1000) -> None:
        self._conn = conn          # RedisConnection
        self._state_key = state_key
        self._lock = threading.Lock()
        # OrderedDict gives O(1) membership and ordered eviction
        self._seen: OrderedDict[str, None] = OrderedDict()
        self._max_seen = max_seen
        self._last_ack: Optional[str] = self._load_last_ack()

    @property
    def last_ack(self) -> Optional[str]:
        with self._lock:
            return self._last_ack

    # -- persistence -----------------------------------------------------

    def _load_last_ack(self) -> Optional[str]:
        try:
            return self._conn.client.hget(self._state_key, "last_ack")
        except Exception:
            return None

    def persist_ack(self, msg_id: str) -> None:
        """Save *msg_id* as last_ack to Redis and memory."""
        try:
            self._conn.client.hset(self._state_key, mapping={"last_ack": msg_id})
        except Exception:
            logger.warning("Failed to persist last_ack=%s", msg_id, exc_info=True)
        with self._lock:
            self._last_ack = msg_id

    # -- dedup -----------------------------------------------------------

    def is_duplicate(self, msg_id: str) -> bool:
        with self._lock:
            if msg_id in self._seen:
                return True
            if self._last_ack and _id_lte(msg_id, self._last_ack):
                return True
            return False

    def record_seen(self, msg_id: str) -> None:
        with self._lock:
            self._seen[msg_id] = None
            while len(self._seen) > self._max_seen:
                self._seen.popitem(last=False)

    # -- audit -----------------------------------------------------------

    def audit_backlog(
        self,
        tasks_stream: str,
        consumer_group: str,
        instance_id: str,
        pending_min_idle_ms: int,
    ) -> Dict[str, Any]:
        """Return a summary dict for observability / audits."""
        summary: Dict[str, Any] = {"last_ack": self.last_ack, "pending": 0, "claimed": 0}
        try:
            pending_info = self._conn.client.xpending(tasks_stream, consumer_group)
            if isinstance(pending_info, dict) and "pending" in pending_info:
                summary["pending"] = pending_info.get("pending", 0)
            else:
                summary["pending"] = pending_info[0] if isinstance(pending_info, (list, tuple)) else 0
        except Exception:
            summary["pending"] = -1
        try:
            claimed = self._conn.client.xautoclaim(
                name=tasks_stream,
                groupname=consumer_group,
                consumername=instance_id,
                min_idle_time=pending_min_idle_ms,
                start_id="0-0",
                count=10,
            )
            if claimed and isinstance(claimed, tuple):
                summary["claimed"] = len(claimed[1])
        except Exception:
            summary["claimed"] = -1
        return summary


# -- helpers (module-level) -----------------------------------------------

def _id_lte(a: str, b: str) -> bool:
    """Compare Redis stream IDs: ``a <= b``."""
    try:
        a1, a2 = a.split("-")
        b1, b2 = b.split("-")
        if int(a1) < int(b1):
            return True
        if int(a1) == int(b1):
            return int(a2) <= int(b2)
        return False
    except Exception:
        return False
