"""Event bus base classes and utilities."""
from __future__ import annotations

import asyncio
import logging
from datetime import UTC, datetime, timedelta
from typing import TYPE_CHECKING, Awaitable, Callable, Iterable

from logos_adk.errors import QueueFullError

from .models import EventRecord

if TYPE_CHECKING:
    from logos_adk.checkpoint_json import JSONObject


_logger = logging.getLogger(__name__)


def _utcnow() -> datetime:
    """Return current UTC datetime."""
    return datetime.now(UTC)


def _utcnow_iso() -> str:
    """Return current UTC datetime as ISO string."""
    return _utcnow().isoformat()


def _iso_after(seconds: float) -> str:
    """Return ISO datetime string after given seconds."""
    return (_utcnow() + timedelta(seconds=max(0.0, float(seconds)))).isoformat()


class EventBus:
    """Abstract persistent event bus."""

    DEFAULT_MAX_ATTEMPTS = 5
    DEFAULT_MAX_QUEUE_DEPTH = 0  # 0 = unlimited

    def __init__(
        self,
        *,
        max_attempts: int | None = None,
        max_queue_depth: int = 0,
    ) -> None:
        """Initialize event bus.
        
        Args:
            max_attempts: Maximum retry attempts before dead-lettering
            max_queue_depth: Maximum queue depth (0 = unlimited)
        """
        self._observers: list[Callable[[EventRecord], Awaitable[None] | None]] = []
        self.max_attempts = max_attempts if max_attempts is not None else self.DEFAULT_MAX_ATTEMPTS
        self.max_queue_depth = max(0, max_queue_depth)

    def on_publish(self, observer: Callable[[EventRecord], Awaitable[None] | None]) -> None:
        """Register a callback for every published event."""
        self._observers.append(observer)

    def _check_backpressure(self) -> None:
        """Raise QueueFullError if queue depth exceeds the configured limit."""
        if self.max_queue_depth <= 0:
            return
        depth = self.queued_count()
        if depth >= self.max_queue_depth:
            raise QueueFullError(
                f"Event bus queue depth ({depth}) exceeds limit ({self.max_queue_depth})"
            )

    def queued_count(self) -> int:
        """Return the number of events in 'queued' or 'claimed' status."""
        return len(self.list_events(status="queued")) + len(self.list_events(status="claimed"))

    async def _notify_observers_async(self, record: EventRecord) -> None:
        """Notify all observers asynchronously."""
        for observer in self._observers:
            try:
                res = observer(record)
                if asyncio.iscoroutine(res):
                    await res
            except Exception:
                _logger.warning(
                    "Event observer %r failed for topic=%s id=%s",
                    observer, record.topic, record.id, exc_info=True,
                )
                continue

    def _notify_observers(self, record: EventRecord) -> None:
        """Notify all observers (sync wrapper)."""
        if not self._observers:
            return
        try:
            loop = asyncio.get_running_loop()
        except RuntimeError:
            asyncio.run(self._notify_observers_async(record))
            return
        task = loop.create_task(self._notify_observers_async(record))
        task.add_done_callback(lambda t: t.exception() if not t.cancelled() and t.exception() else 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.
        
        Args:
            topic: Event topic
            payload: Event payload
            campaign_id: Optional campaign identifier
            correlation_id: Optional correlation identifier
            run_id: Optional run identifier
            publisher: Optional publisher name
            dedupe_key: Optional deduplication key
            available_in_seconds: Delay before event becomes available
            
        Returns:
            Published EventRecord
        """
        raise NotImplementedError

    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.
        
        Args:
            consumer: Consumer identifier
            topics: Topics to claim from
            lease_seconds: Lease duration
            correlation_id: Optional correlation filter
            run_id: Optional run filter
            
        Returns:
            Claimed EventRecord or None
        """
        raise NotImplementedError

    def ack(self, event_id: str, consumer: str) -> None:
        """Acknowledge event completion.
        
        Args:
            event_id: Event identifier
            consumer: Consumer identifier
        """
        raise NotImplementedError

    def requeue(
        self,
        event_id: str,
        consumer: str,
        *,
        delay_seconds: float = 0.0,
        error: str | None = None,
    ) -> None:
        """Requeue event for retry.
        
        Args:
            event_id: Event identifier
            consumer: Consumer identifier
            delay_seconds: Delay before requeue
            error: Optional error message
        """
        raise NotImplementedError

    def get_event(self, event_id: str) -> EventRecord | None:
        """Get event by ID.
        
        Args:
            event_id: Event identifier
            
        Returns:
            EventRecord or None
        """
        raise NotImplementedError

    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.
        
        Args:
            topic: Filter by topic
            status: Filter by status
            campaign_id: Filter by campaign
            correlation_id: Filter by correlation
            run_id: Filter by run
            
        Returns:
            List of EventRecords
        """
        raise NotImplementedError

    def list_dead_letters(self, *, topic: str | None = None) -> list[EventRecord]:
        """Return events that exceeded max_attempts."""
        return self.list_events(topic=topic, status="dead_letter")

    def requeue_dead_letter(self, event_id: str, *, delay_seconds: float = 0.0) -> bool:
        """Move a dead-lettered event back to 'queued' for retry.
        
        Args:
            event_id: Event identifier
            delay_seconds: Delay before requeue
            
        Returns:
            True if requeued
        """
        raise NotImplementedError

    def purge_completed(self, *, max_age_seconds: float = 86400.0) -> int:
        """Delete completed/dead_letter events older than max_age_seconds.
        
        Args:
            max_age_seconds: Maximum age in seconds
            
        Returns:
            Count of deleted events
        """
        raise NotImplementedError


def _validate_topic(topic: str | None) -> str:
    """Validate and normalize topic string."""
    if not topic:
        raise ValueError("topic is required")
    return str(topic).strip()


def _clone_record(record: EventRecord) -> EventRecord:
    """Create a deep copy of an EventRecord."""
    from logos_adk.checkpoint_json import clone_json_object
    
    return EventRecord(
        id=record.id,
        topic=record.topic,
        payload=clone_json_object(record.payload),
        status=record.status,
        created_at=record.created_at,
        updated_at=record.updated_at,
        available_at=record.available_at,
        campaign_id=record.campaign_id,
        correlation_id=record.correlation_id,
        run_id=record.run_id,
        publisher=record.publisher,
        lease_owner=record.lease_owner,
        lease_expires_at=record.lease_expires_at,
        attempt=record.attempt,
        dedupe_key=record.dedupe_key,
        last_error=record.last_error,
    )


def _validate_lease_owner(record: EventRecord, consumer: str) -> None:
    """Validate that consumer owns the lease."""
    if record.lease_owner != consumer:
        raise ValueError(
            f"Consumer {consumer!r} does not own lease for event {record.id} "
            f"(owner={record.lease_owner!r})"
        )


__all__ = [
    "EventBus",
    "_utcnow",
    "_utcnow_iso",
    "_iso_after",
    "_validate_topic",
    "_clone_record",
    "_validate_lease_owner",
]
