"""PyRapide → iKnowhy bridge adapter.

Powered by PyRapide (https://github.com/ShaneDolphin/pyrapide) by Shane Morris.
PyRapide implements RAPIDE causal event-driven architecture modeling for Python.
This adapter bridges PyRapide's Computation/StreamProcessor into iKnowhy's
persistent causal event log, giving you durable storage, a web dashboard,
and API access to every causal chain your PyRapide architecture produces.

Usage (batch — after engine.run()):
    from iknowhy import CausewayClient
    from iknowhy.pyrapide import PyRapideAdapter

    client = CausewayClient(api_key="cwy_...")
    adapter = PyRapideAdapter(client, session_id="my-app")

    engine = Engine()
    comp = await engine.run(arch, timeout=5.0)
    event_ids = await adapter.flush_computation(comp)

Usage (streaming — real-time via StreamProcessor):
    processor = StreamProcessor()
    processor.add_source("my-source", source)
    adapter.attach_stream(processor, session_id="live-run")

    await processor.run()
"""
from __future__ import annotations

import asyncio
from typing import TYPE_CHECKING, Optional

if TYPE_CHECKING:
    from pyrapide.core.computation import Computation
    from pyrapide.core.event import Event
    from pyrapide.runtime.streaming import StreamProcessor

from iknowhy._client import CausewayClient


class PyRapideAdapter:
    """Bridges PyRapide causal computations into iKnowhy's persistent event log.

    Powered by PyRapide (https://pypi.org/project/pyrapide/) by Shane Morris.
    PyRapide brings the RAPIDE causal architecture model — developed at Stanford
    by David Luckham — to modern async Python.

    Args:
        client: An initialized CausewayClient.
        session_id: Optional default session ID for all logged events.
            Can be overridden per call.
        tags: Optional default tags applied to every event.
    """

    def __init__(
        self,
        client: CausewayClient,
        session_id: Optional[str] = None,
        tags: Optional[dict] = None,
    ):
        self.client = client
        self.session_id = session_id
        self.tags = tags or {}
        # Maps PyRapide event.id → iKnowhy event_id
        self._id_map: dict[str, str] = {}

    # ------------------------------------------------------------------
    # Batch mode: flush a completed Computation
    # ------------------------------------------------------------------

    async def flush_computation(
        self,
        computation: "Computation",
        session_id: Optional[str] = None,
        tags: Optional[dict] = None,
    ) -> list[str]:
        """Upload all events in a completed PyRapide Computation to iKnowhy.

        Events are sent in topological (causal) order so that cause_ids
        always reference events that have already been registered.

        Args:
            computation: A completed PyRapide Computation from engine.run().
            session_id: Override session ID for this batch.
            tags: Additional tags merged with instance-level tags.

        Returns:
            List of iKnowhy event IDs in topological order.
        """
        sid = session_id or self.session_id
        merged_tags = {**self.tags, **(tags or {})}
        iknowhy_ids: list[str] = []

        loop = asyncio.get_event_loop()
        poset = computation._poset

        for pyrapide_event in computation.topological_order():
            # Resolve causal parent IDs in iKnowhy's ID space
            cause_ids = [
                self._id_map[cause.id]
                for cause in poset.causes(pyrapide_event)
                if cause.id in self._id_map
            ]

            iknowhy_id = await loop.run_in_executor(
                None,
                lambda e=pyrapide_event, cids=cause_ids: self.client.log(
                    name=e.name,
                    payload={**e.payload, "_pyrapide_source": e.source} if e.source else e.payload,
                    session_id=sid,
                    cause_ids=cids or None,
                    tags={**merged_tags, "_pyrapide_id": e.id} if merged_tags or True else None,
                ),
            )

            self._id_map[pyrapide_event.id] = iknowhy_id
            iknowhy_ids.append(iknowhy_id)

        return iknowhy_ids

    # ------------------------------------------------------------------
    # Streaming mode: attach to a live StreamProcessor
    # ------------------------------------------------------------------

    def attach_stream(
        self,
        processor: "StreamProcessor",
        session_id: Optional[str] = None,
        tags: Optional[dict] = None,
    ) -> None:
        """Register a real-time on_event hook with a PyRapide StreamProcessor.

        Each event is shipped to iKnowhy as it arrives, preserving causal
        links to any already-logged parent events.

        Args:
            processor: A configured (but not yet running) StreamProcessor.
            session_id: Session ID for streamed events.
            tags: Additional tags for all streamed events.
        """
        sid = session_id or self.session_id
        merged_tags = {**self.tags, **(tags or {})}

        async def on_event(event: "Event") -> None:
            try:
                # Best-effort: look up already-logged cause IDs
                poset = processor.computation._poset
                cause_ids = [
                    self._id_map[c.id]
                    for c in poset.causes(event)
                    if c.id in self._id_map
                ]

                loop = asyncio.get_event_loop()
                iknowhy_id = await loop.run_in_executor(
                    None,
                    lambda: self.client.log(
                        name=event.name,
                        payload={**event.payload, "_pyrapide_source": event.source}
                        if event.source
                        else event.payload,
                        session_id=sid,
                        cause_ids=cause_ids or None,
                        tags={**merged_tags, "_pyrapide_id": event.id},
                    ),
                )
                self._id_map[event.id] = iknowhy_id
            except Exception:
                # Fire-and-forget: never let logging failures affect the computation
                pass

        processor.on_event(on_event)

    # ------------------------------------------------------------------
    # Utility
    # ------------------------------------------------------------------

    def id_map(self) -> dict[str, str]:
        """Return the PyRapide event.id → iKnowhy event_id mapping."""
        return dict(self._id_map)

    def clear(self) -> None:
        """Reset the internal ID map (e.g. between separate computations)."""
        self._id_map.clear()
