"""Streaming convenience -- SlotSession for BitBlock event streaming.

``SlotSession`` wraps the slot lifecycle and BitBlock submission into a
single context-manager-friendly object.  It is the primary interface for
products (Grid, Dominion, Sonic, Stillpoint) to stream events into SBN
and receive attested receipts.

Usage::

    from sbn import SbnClient

    client = SbnClient(base_url="https://api.smartblocks.network")
    client.authenticate_api_key("sbn_live_...")

    with client.open_slot(
        worker_id="worker-1",
        task_type="grid.atomic_rentals",
        domain="grid.atomic_rentals",
        generation_interval=60,
    ) as slot:
        # Stream events as they happen
        slot.action("vehicle_checkout", score=0.9)
        slot.action("delivery_completed", score=0.88)
        slot.interaction(score=0.92, weight=1.0)

        # Check what's been composed so far
        for bb in slot.list_bitblocks():
            print(bb.block_type, bb.composed_block_id)

    # slot.__exit__ auto-closes and returns the receipt
    print(slot.receipt)
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, List, Mapping, TYPE_CHECKING

from sbn.bitblocks import BitBlock, BitBlockList, BitBlocksClient
from sbn.gateway import GatewayClient, SlotClosure, SlotHandle

if TYPE_CHECKING:
    from sbn._http import HttpTransport


@dataclass(slots=True)
class SlotReceipt:
    """Receipt info returned when a slot closes, including compose receipts."""

    slot_id: str
    receipt_id: str | None = None
    closed_at: str | None = None
    blocks_composed: int = 0
    blocks_attested: int = 0
    compose_receipts: list[dict[str, Any]] = field(default_factory=list)

    @classmethod
    def from_closure(cls, closure: SlotClosure) -> SlotReceipt:
        return cls(
            slot_id=closure.slot_id,
            receipt_id=closure.receipt_id,
            closed_at=closure.closed_at.isoformat() if closure.closed_at else None,
            blocks_composed=closure.blocks_composed,
            blocks_attested=closure.blocks_attested,
            compose_receipts=closure.compose_receipts,
        )


class SlotSession:
    """Context manager for streaming BitBlock events into a slot.

    Manages the full lifecycle: open slot -> stream events -> close slot.
    Provides convenience methods for Action, Interaction, and Events
    BitBlock submission so callers don't need to construct raw payloads.

    Parameters
    ----------
    gateway:
        GatewayClient for slot open/close.
    bitblocks_client:
        BitBlocksClient for BitBlock submission.
    worker_id:
        Worker identifier for the slot.
    task_type:
        Task type / domain key for the slot.
    domain:
        BitBlock domain namespace.
    generation_interval:
        Seconds between generation boundaries (default: 60).
    metadata:
        Optional metadata dict for the slot.
    """

    def __init__(
        self,
        *,
        gateway: GatewayClient,
        bitblocks_client: BitBlocksClient,
        worker_id: str,
        task_type: str,
        domain: str,
        generation_interval: int = 60,
        metadata: Mapping[str, Any] | None = None,
    ) -> None:
        self._gateway = gateway
        self._bitblocks = bitblocks_client
        self._worker_id = worker_id
        self._task_type = task_type
        self._domain = domain
        self._generation_interval = generation_interval
        self._metadata = dict(metadata) if metadata else {}

        self._slot: SlotHandle | None = None
        self._receipt: SlotReceipt | None = None
        self._submitted: List[BitBlock] = []

    # ── Properties ────────────────────────────────────────────────────

    @property
    def slot_id(self) -> str:
        """The slot ID (available after open / __enter__)."""
        if self._slot is None:
            raise RuntimeError("SlotSession not opened yet")
        return self._slot.id

    @property
    def slot(self) -> SlotHandle | None:
        """The SlotHandle, or None if not yet opened."""
        return self._slot

    @property
    def receipt(self) -> SlotReceipt | None:
        """The SlotReceipt, available after close()."""
        return self._receipt

    @property
    def submitted(self) -> List[BitBlock]:
        """All BitBlocks submitted in this session."""
        return list(self._submitted)

    # ── Lifecycle ─────────────────────────────────────────────────────

    def open(self) -> SlotHandle:
        """Open the slot. Called automatically by __enter__."""
        slot_meta = {
            **self._metadata,
            "generation_interval": self._generation_interval,
        }
        self._slot = self._gateway.create_slot(
            worker_id=self._worker_id,
            task_type=self._task_type,
            generation_interval=self._generation_interval,
            metadata=slot_meta,
        )
        return self._slot

    def close(self) -> SlotReceipt:
        """Close the slot and return the receipt. Called automatically by __exit__."""
        if self._slot is None:
            raise RuntimeError("SlotSession not opened yet")
        closure = self._gateway.close_slot(self._slot.id)
        self._receipt = SlotReceipt.from_closure(closure)
        return self._receipt

    def __enter__(self) -> SlotSession:
        self.open()
        return self

    def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        if self._slot is not None and self._receipt is None:
            try:
                self.close()
            except Exception:
                pass  # Don't mask the original exception

    # ── BitBlock submission ───────────────────────────────────────────

    def action(
        self,
        action_name: str,
        *,
        score: float = 1.0,
        reinforced: bool = True,
        metadata: Mapping[str, Any] | None = None,
        **extra_payload: Any,
    ) -> BitBlock:
        """Submit an Action BitBlock.

        Parameters
        ----------
        action_name:
            The action identifier (e.g. ``"vehicle_checkout"``).
        score:
            Numeric score for this action (0.0 - 1.0).
        reinforced:
            Whether this action was positively reinforced.
        metadata:
            Optional metadata for this specific BitBlock.
        **extra_payload:
            Additional key-value pairs merged into the payload.
        """
        payload: dict[str, Any] = {
            "action": action_name,
            "score": score,
            "reinforced": reinforced,
            **extra_payload,
        }
        bb_meta = dict(metadata) if metadata else {}
        bb_meta.setdefault("worker_id", self._worker_id)

        bb = self._bitblocks.submit(
            slot_id=self.slot_id,
            block_type="Action",
            domain=self._domain,
            payload=payload,
            metadata=bb_meta,
        )
        self._submitted.append(bb)
        return bb

    def interaction(
        self,
        *,
        score: float,
        weight: float = 1.0,
        metadata: Mapping[str, Any] | None = None,
        **extra_payload: Any,
    ) -> BitBlock:
        """Submit an Interaction BitBlock.

        Parameters
        ----------
        score:
            Interaction quality score (0.0 - 1.0).
        weight:
            Weight factor for this interaction.
        metadata:
            Optional metadata for this specific BitBlock.
        **extra_payload:
            Additional key-value pairs merged into the payload.
        """
        payload: dict[str, Any] = {
            "score": score,
            "weight": weight,
            **extra_payload,
        }
        bb_meta = dict(metadata) if metadata else {}
        bb_meta.setdefault("worker_id", self._worker_id)

        bb = self._bitblocks.submit(
            slot_id=self.slot_id,
            block_type="Interaction",
            domain=self._domain,
            payload=payload,
            metadata=bb_meta,
        )
        self._submitted.append(bb)
        return bb

    def event(
        self,
        payload: Mapping[str, Any],
        *,
        metadata: Mapping[str, Any] | None = None,
    ) -> BitBlock:
        """Submit an Events BitBlock with arbitrary payload.

        Parameters
        ----------
        payload:
            Arbitrary event data.
        metadata:
            Optional metadata for this specific BitBlock.
        """
        bb_meta = dict(metadata) if metadata else {}
        bb_meta.setdefault("worker_id", self._worker_id)

        bb = self._bitblocks.submit(
            slot_id=self.slot_id,
            block_type="Events",
            domain=self._domain,
            payload=dict(payload),
            metadata=bb_meta,
        )
        self._submitted.append(bb)
        return bb

    def submit_raw(
        self,
        *,
        block_type: str,
        payload: Mapping[str, Any],
        metadata: Mapping[str, Any] | None = None,
    ) -> BitBlock:
        """Submit a BitBlock with explicit block_type and payload.

        Use this for custom block types or when you need full control
        over the payload structure.
        """
        bb = self._bitblocks.submit(
            slot_id=self.slot_id,
            block_type=block_type,
            domain=self._domain,
            payload=dict(payload),
            metadata=dict(metadata) if metadata else None,
        )
        self._submitted.append(bb)
        return bb

    # ── Queries ───────────────────────────────────────────────────────

    def list_bitblocks(
        self,
        *,
        generation: int | None = None,
    ) -> List[BitBlock]:
        """List BitBlocks in this slot, optionally filtered by generation."""
        result = self._bitblocks.list(self.slot_id, generation=generation)
        return result.bitblocks

    def list_slot_blocks(self) -> dict[str, Any]:
        """List composed SmartBlocks for this slot."""
        return self._gateway.list_slot_blocks(self.slot_id)
