"""
cortex/events/emitter.py — In-process event bus (M19 / #183).

Architecture
------------
The event system is intentionally simple:

  EventType   — enum of all first-class lifecycle events
  CortexEvent — immutable dataclass representing a single event
  EventEmitter — stateful emitter that:
                    1. Persists every event to the DB (event_log table)
                    2. Notifies registered in-process listeners synchronously

Design decisions:
  - Listeners are called in registration order.
  - Listener exceptions are caught and logged; they never propagate to the caller.
  - Storage writes are awaited before listeners are notified (DB is the source of truth).
  - Webhooks are *not* in scope for v1.0 — see M20 in v1.1 roadmap.

Dependencies: cortex.storage.adapter (StorageAdapter)
Dependents:   CortexPlugin (wires emitter into all modules)
"""
from __future__ import annotations

import json
import logging
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import Any, Callable, Dict, List, Optional

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# EventType
# ---------------------------------------------------------------------------

class EventType(str, Enum):
    """First-class lifecycle events emitted by Cortex modules.

    Using ``str`` as a mixin lets values be JSON-serialised directly without
    calling ``.value`` everywhere.
    """

    # Memory / claim lifecycle
    MEMORY_CREATED = "memory.created"
    MEMORY_UPDATED = "memory.updated"
    MEMORY_DELETED = "memory.deleted"
    MEMORY_PROMOTED = "memory.promoted"
    MEMORY_DEPRECATED = "memory.deprecated"

    # Entity lifecycle
    ENTITY_CREATED = "entity.created"
    ENTITY_MERGED = "entity.merged"
    ENTITY_FORGOTTEN = "entity.forgotten"  # GDPR hard-delete (#108)

    # Cornerstone lifecycle
    CORNERSTONE_SEALED = "cornerstone.sealed"
    CORNERSTONE_DRIFT_DETECTED = "cornerstone.drift_detected"

    # Session lifecycle
    SESSION_STARTED = "session.started"
    SESSION_ENDED = "session.ended"

    # Circadian / dream cycle
    DREAM_STARTED = "dream.started"
    DREAM_COMPLETED = "dream.completed"
    SLEEP_COMPLETED = "sleep.completed"

    # Feedback
    FEEDBACK_SUBMITTED = "feedback.submitted"


# ---------------------------------------------------------------------------
# CortexEvent
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class CortexEvent:
    """Immutable record of a single Cortex lifecycle event.

    Attributes
    ----------
    event_type:
        One of the EventType enum values.
    timestamp:
        ISO-8601 UTC timestamp string (e.g. ``"2026-03-07T12:00:00+00:00"``).
        If not provided, defaults to *now* (UTC).
    owner_id:
        The owner/user scope this event belongs to.
    payload:
        Arbitrary JSON-serialisable dict with event-specific data.
    session_id:
        Optional session ID to associate the event with a session.
    """

    event_type: EventType
    owner_id: str = "default"
    timestamp: str = field(
        default_factory=lambda: datetime.now(timezone.utc).isoformat()
    )
    payload: Dict[str, Any] = field(default_factory=dict)
    session_id: Optional[str] = None


# ---------------------------------------------------------------------------
# EventEmitter
# ---------------------------------------------------------------------------

# Listener type: async or sync callable that receives a CortexEvent
ListenerFn = Callable[["CortexEvent"], Any]


class EventEmitter:
    """In-process event emitter for Cortex lifecycle events.

    Parameters
    ----------
    storage:
        A StorageAdapter instance. Can be ``None`` for testing without a DB
        (events will still be dispatched to listeners but not persisted).

    Example
    -------
    ::

        emitter = EventEmitter(storage=plugin.storage)

        @emitter.on(EventType.MEMORY_CREATED)
        def _on_create(event):
            print("New memory:", event.payload)

        await emitter.emit(CortexEvent(
            event_type=EventType.MEMORY_CREATED,
            owner_id="alice",
            payload={"claim_id": "c1"},
        ))
    """

    def __init__(self, storage: Any = None, webhook_dispatcher: Any = None) -> None:
        self._storage = storage
        # Optional WebhookDispatcher — wired in by CortexPlugin for outbound delivery
        self._webhook_dispatcher = webhook_dispatcher
        # Map from EventType → ordered list of listeners
        self._listeners: Dict[EventType, List[ListenerFn]] = {}

    # ------------------------------------------------------------------ #
    # Registration
    # ------------------------------------------------------------------ #

    def on(
        self,
        event_type: EventType,
        callback: Optional[ListenerFn] = None,
    ):
        """Register *callback* to be called when *event_type* is emitted.

        Supports both direct call and decorator factory usage::

            # Direct call
            emitter.on(EventType.SESSION_STARTED, my_handler)

            # Decorator factory
            @emitter.on(EventType.SESSION_STARTED)
            def my_handler(event): ...

        Parameters
        ----------
        event_type:
            The EventType to subscribe to.
        callback:
            A callable (sync or async) that accepts a single CortexEvent.
            If omitted the method returns a decorator.

        Returns
        -------
        If *callback* is provided: the callback (unchanged).
        If *callback* is omitted: a decorator that registers and returns
        the decorated function.
        """
        def _register(fn: ListenerFn) -> ListenerFn:
            if event_type not in self._listeners:
                self._listeners[event_type] = []
            self._listeners[event_type].append(fn)
            logger.debug("Registered listener for %s: %s", event_type, fn)
            return fn

        if callback is not None:
            return _register(callback)
        # Decorator factory — @emitter.on(EventType.X)
        return _register

    def off(self, event_type: EventType, callback: ListenerFn) -> None:
        """Unregister a previously registered listener.

        Silently does nothing if *callback* was not registered.
        """
        listeners = self._listeners.get(event_type, [])
        try:
            listeners.remove(callback)
        except ValueError:
            pass

    @property
    def listeners(self) -> Dict[EventType, List[ListenerFn]]:
        """Read-only view of registered listeners (keyed by EventType)."""
        return dict(self._listeners)

    def listener_count(self, event_type: Optional[EventType] = None) -> int:
        """Return the total number of registered listeners.

        If *event_type* is given, count only listeners for that type.
        """
        if event_type is not None:
            return len(self._listeners.get(event_type, []))
        return sum(len(v) for v in self._listeners.values())

    # ------------------------------------------------------------------ #
    # Emission
    # ------------------------------------------------------------------ #

    async def emit(self, event: CortexEvent) -> None:
        """Emit *event*: persist to DB then notify all listeners.

        Persistence is attempted first. If storage is unavailable (None or
        raises), the error is logged but listeners are still notified so that
        in-process reactors continue to work even in degraded conditions.

        Listener exceptions are caught individually and logged; they never
        cancel subsequent listeners or propagate to the caller.

        Parameters
        ----------
        event:
            The CortexEvent to emit.
        """
        # 1. Persist to event_log
        await self._persist(event)

        # 2. Notify in-process listeners
        await self._dispatch(event)

        # 3. Dispatch to outbound webhooks (fire-and-forget)
        await self._dispatch_webhooks(event)

    async def _persist(self, event: CortexEvent) -> None:
        """Write event to the event_log table via storage adapter."""
        if self._storage is None:
            logger.debug("EventEmitter: no storage — skipping persist for %s", event.event_type)
            return
        try:
            payload_json = json.dumps(event.payload) if event.payload else None
            await self._storage.log_event(
                event_type=event.event_type.value if isinstance(event.event_type, EventType) else str(event.event_type),
                owner_id=event.owner_id,
                timestamp=event.timestamp,
                payload_json=payload_json,
                session_id=event.session_id,
            )
            logger.debug("EventEmitter: persisted %s for owner=%s", event.event_type, event.owner_id)
        except Exception as exc:
            logger.warning("EventEmitter: failed to persist event %s: %s", event.event_type, exc)

    async def _dispatch(self, event: CortexEvent) -> None:
        """Invoke all registered listeners for event.event_type."""
        import asyncio

        listeners = self._listeners.get(event.event_type, [])
        for listener in listeners:
            try:
                result = listener(event)
                if asyncio.iscoroutine(result):
                    await result
            except Exception as exc:
                logger.warning(
                    "EventEmitter: listener %s raised for %s: %s",
                    listener,
                    event.event_type,
                    exc,
                )

    async def _dispatch_webhooks(self, event: CortexEvent) -> None:
        """Forward event to the WebhookDispatcher if one is configured."""
        if self._webhook_dispatcher is None:
            return
        try:
            await self._webhook_dispatcher.dispatch(event)
        except Exception as exc:
            logger.warning("EventEmitter: webhook dispatch failed for %s: %s", event.event_type, exc)
