"""In-memory event bus implementation."""
from __future__ import annotations

import logging
from datetime import datetime, timedelta
from typing import Iterable
from uuid import uuid4

from logos_adk.checkpoint_json import JSONObject, clone_json_object, ensure_json_object

from .base import (
    EventBus,
    _clone_record,
    _iso_after,
    _utcnow,
    _utcnow_iso,
    _validate_topic,
    _validate_lease_owner,
)
from .models import EventRecord

_logger = logging.getLogger(__name__)


class InMemoryEventBus(EventBus):
    """Simple in-memory event bus for tests and local execution."""

    _MAX_COMPLETED_EVENTS = 10_000

    def __init__(
        self,
        *,
        max_attempts: int | None = None,
        max_queue_depth: int = 0,
    ) -> None:
        """Initialize in-memory event bus.
        
        Args:
            max_attempts: Maximum retry attempts before dead-lettering
            max_queue_depth: Maximum queue depth (0 = unlimited)
        """
        super().__init__(max_attempts=max_attempts, max_queue_depth=max_queue_depth)
        self._events: dict[str, EventRecord] = {}

    def queued_count(self) -> int:
        """Return count of queued/claimed events."""
        return sum(1 for r in self._events.values() if r.status in ("queued", "claimed"))

    def _gc_completed(self) -> None:
        """Evict oldest completed events when limit is exceeded."""
        completed = [
            (rec.updated_at, eid)
            for eid, rec in self._events.items()
            if rec.status == "completed"
        ]
        if len(completed) <= self._MAX_COMPLETED_EVENTS:
            return
        completed.sort()
        to_remove = len(completed) - self._MAX_COMPLETED_EVENTS
        for _, eid in completed[:to_remove]:
            self._events.pop(eid, None)

    def publish(
        self,
        topic: str,
        payload: JSONObject,
        *,
        campaign_id: str | None = None,
        correlation_id: str | None = None,
        run_id: str | None = None,
        publisher: str | None = None,
        dedupe_key: str | None = None,
        available_in_seconds: float = 0.0,
    ) -> EventRecord:
        """Publish an event to the bus."""
        self._check_backpressure()
        topic_name = _validate_topic(topic)
        payload_data = ensure_json_object(payload, path=f"event[{topic_name}].payload")
        record = EventRecord(
            id=uuid4().hex,
            topic=topic_name,
            payload=clone_json_object(payload_data, path=f"event[{topic_name}].payload"),
            status="queued",
            created_at=_utcnow_iso(),
            updated_at=_utcnow_iso(),
            available_at=_iso_after(available_in_seconds),
            campaign_id=campaign_id,
            correlation_id=correlation_id,
            run_id=run_id,
            publisher=publisher,
            dedupe_key=dedupe_key,
        )
        self._events[record.id] = record
        self._notify_observers(record)
        return _clone_record(record)

    def claim(
        self,
        consumer: str,
        topics: Iterable[str],
        *,
        lease_seconds: float = 60.0,
        correlation_id: str | None = None,
        run_id: str | None = None,
    ) -> EventRecord | None:
        """Claim an event for processing."""
        topic_names = {_validate_topic(topic) for topic in topics}
        if not topic_names:
            return None
        now = _utcnow()
        for record in sorted(self._events.values(), key=lambda item: (item.available_at, item.created_at, item.id)):
            if record.topic not in topic_names:
                continue
            if correlation_id is not None and record.correlation_id != correlation_id:
                continue
            if run_id is not None and record.run_id != run_id:
                continue
            available_at = datetime.fromisoformat(record.available_at)
            lease_expires_at = (
                datetime.fromisoformat(record.lease_expires_at)
                if record.lease_expires_at
                else None
            )
            claimable = (
                record.status == "queued" and available_at <= now
            ) or (
                record.status == "claimed"
                and lease_expires_at is not None
                and lease_expires_at <= now
            )
            if not claimable:
                continue
            record.status = "claimed"
            record.updated_at = now.isoformat()
            record.lease_owner = consumer
            record.lease_expires_at = (now + timedelta(seconds=max(0.0, float(lease_seconds)))).isoformat()
            record.attempt += 1
            record.last_error = None
            return _clone_record(record)
        return None

    def ack(self, event_id: str, consumer: str) -> None:
        """Acknowledge event completion."""
        record = self._events.get(event_id)
        if record is None:
            return
        _validate_lease_owner(record, consumer)
        record.status = "completed"
        record.updated_at = _utcnow_iso()
        record.lease_owner = None
        record.lease_expires_at = None
        self._gc_completed()

    def requeue(
        self,
        event_id: str,
        consumer: str,
        *,
        delay_seconds: float = 0.0,
        error: str | None = None,
    ) -> None:
        """Requeue event for retry."""
        record = self._events.get(event_id)
        if record is None:
            return
        _validate_lease_owner(record, consumer)
        # Dead Letter Queue: if max_attempts exceeded, move to dead_letter
        if self.max_attempts > 0 and record.attempt >= self.max_attempts:
            record.status = "dead_letter"
            record.updated_at = _utcnow_iso()
            record.lease_owner = None
            record.lease_expires_at = None
            record.last_error = error
            _logger.warning(
                "Event '%s' (topic=%s) moved to dead letter queue after %d attempts",
                event_id, record.topic, record.attempt,
            )
            return
        record.status = "queued"
        record.updated_at = _utcnow_iso()
        record.available_at = _iso_after(delay_seconds)
        record.lease_owner = None
        record.lease_expires_at = None
        record.last_error = error

    def requeue_dead_letter(self, event_id: str, *, delay_seconds: float = 0.0) -> bool:
        """Requeue a dead-lettered event."""
        record = self._events.get(event_id)
        if record is None or record.status != "dead_letter":
            return False
        record.status = "queued"
        record.attempt = 0
        record.updated_at = _utcnow_iso()
        record.available_at = _iso_after(delay_seconds)
        record.lease_owner = None
        record.lease_expires_at = None
        record.last_error = None
        return True

    def purge_completed(self, *, max_age_seconds: float = 86400.0) -> int:
        """Purge old completed events."""
        cutoff = (_utcnow() - timedelta(seconds=max_age_seconds)).isoformat()
        to_delete = [
            eid for eid, rec in self._events.items()
            if rec.status in ("completed", "dead_letter") and rec.updated_at < cutoff
        ]
        for eid in to_delete:
            del self._events[eid]
        return len(to_delete)

    def get_event(self, event_id: str) -> EventRecord | None:
        """Get event by ID."""
        record = self._events.get(event_id)
        return None if record is None else _clone_record(record)

    def list_events(
        self,
        *,
        topic: str | None = None,
        status: str | None = None,
        campaign_id: str | None = None,
        correlation_id: str | None = None,
        run_id: str | None = None,
    ) -> list[EventRecord]:
        """List events with filters."""
        topic_name = _validate_topic(topic) if topic is not None else None
        records = []
        for record in self._events.values():
            if topic_name is not None and record.topic != topic_name:
                continue
            if status is not None and record.status != status:
                continue
            if campaign_id is not None and record.campaign_id != campaign_id:
                continue
            if correlation_id is not None and record.correlation_id != correlation_id:
                continue
            if run_id is not None and record.run_id != run_id:
                continue
            records.append(_clone_record(record))
        return sorted(records, key=lambda item: (item.created_at, item.id))


__all__ = ["InMemoryEventBus"]
