"""
Novyx Async SDK Client

Async-native client using httpx. Covers core memory, eval, and sentinel intel.
Control methods (action_submit, etc.) require the sync client.

Requires: pip install novyx[async]

Usage:
    from novyx import AsyncNovyx

    async with AsyncNovyx(api_key="nram_...") as nx:
        await nx.remember("User prefers dark mode", tags=["ui"])
        result = await nx.recall("user preferences")
        score = await nx.eval_run()
"""

import json as _json
from typing import Any, Dict, Iterator, List, Optional

try:
    import httpx
except ImportError:
    raise ImportError(
        "httpx is required for AsyncNovyx. Install with: pip install novyx[async]"
    )

from .client import Memory, SearchResult, ListResult
from .exceptions import (
    NovyxError,
    NovyxAuthError,
    NovyxForbiddenError,
    NovyxRateLimitError,
    NovyxNotFoundError,
)


class AsyncNovyx:
    """
    Async Novyx SDK client for core memory, eval, and sentinel intel.

    Features:
    - **Memory**: remember(), recall(), memories(), memory(), forget()
    - **Rollback**: rollback(), rollback_preview(), rollback_history()
    - **Audit**: audit(), audit_export(), audit_verify()
    - **Eval**: eval_run(), eval_gate(), eval_history(), eval_drift()
    - **Traces**: trace_create(), trace_step(), trace_complete(), trace_verify()

    Example:
        >>> from novyx import AsyncNovyx
        >>> async with AsyncNovyx(api_key="nram_your_key") as nx:
        ...     await nx.remember("User prefers dark mode")
        ...     result = await nx.recall("user preferences")
        ...     health = await nx.eval_run()
        ...     print(f"Memory health: {health['health_score']}")
    """

    def __init__(
        self,
        api_key: str,
        api_url: str = "https://novyx-ram-api.fly.dev",
        timeout: int = 30,
        agent_id: Optional[str] = None,
        source: Optional[str] = None,
    ):
        self.api_key = api_key
        self.api_url = api_url.rstrip("/")
        self.timeout = timeout
        self.agent_id = agent_id
        self._source = source
        self._client: Optional[httpx.AsyncClient] = None
        self._validate_key()

    def _validate_key(self) -> None:
        if not self.api_key or not self.api_key.startswith("nram_"):
            raise NovyxAuthError("Invalid API key format. Expected: nram_<tenant>_<signature>")

    def _headers(self) -> Dict[str, str]:
        h = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json",
            "User-Agent": f"novyx-sdk/3.0.0",
        }
        if self._source:
            h["X-Novyx-Source"] = self._source
        return h

    async def _ensure_client(self) -> httpx.AsyncClient:
        if self._client is None or self._client.is_closed:
            self._client = httpx.AsyncClient(timeout=self.timeout)
        return self._client

    async def close(self):
        """Close the underlying HTTP client."""
        if self._client and not self._client.is_closed:
            await self._client.aclose()

    async def __aenter__(self):
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await self.close()

    async def _request(
        self,
        method: str,
        endpoint: str,
        params: Optional[Dict] = None,
        json_data: Optional[Dict] = None,
        raw_response: bool = False,
    ) -> Any:
        """Make an async API request with error handling."""
        url = f"{self.api_url}{endpoint}"
        client = await self._ensure_client()

        try:
            response = await client.request(
                method=method,
                url=url,
                headers=self._headers(),
                params=params,
                json=json_data,
            )

            if response.status_code == 401:
                raise NovyxAuthError("Invalid API key or unauthorized")

            if response.status_code == 403:
                data = response.json() if response.text else {}
                raise NovyxForbiddenError(data.get("error", "Forbidden"), data)

            if response.status_code == 404:
                data = response.json() if response.text else {}
                raise NovyxNotFoundError(data.get("error", "Not found"))

            if response.status_code == 429:
                data = response.json() if response.text else {}
                raise NovyxRateLimitError(data.get("error", "Rate limit exceeded"), data)

            if response.status_code == 204 or not response.text:
                return {} if not raw_response else response

            if raw_response:
                response.raise_for_status()
                return response

            try:
                response_data = response.json()
            except Exception:
                if response.status_code >= 400:
                    raise NovyxError(f"HTTP {response.status_code}: {response.text[:200]}")
                raise NovyxError(f"Invalid JSON response: {response.text[:200]}")

            if response.status_code >= 400:
                raise NovyxError(f"HTTP {response.status_code}: {response_data}")

            return response_data

        except httpx.TimeoutException:
            raise NovyxError(f"Request timed out after {self.timeout}s")
        except httpx.ConnectError:
            raise NovyxError(f"Failed to connect to {self.api_url}")
        except httpx.RequestError as e:
            raise NovyxError(f"Request failed: {e}")

    # ── Memory ────────────────────────────────────────────────────────

    async def remember(
        self,
        observation: str,
        *,
        tags: Optional[List[str]] = None,
        context: Optional[str] = None,
        importance: int = 5,
        agent_id: Optional[str] = None,
        space_id: Optional[str] = None,
        metadata: Optional[Dict[str, Any]] = None,
        ttl_seconds: Optional[int] = None,
        auto_link: bool = False,
        conflict_strategy: Optional[str] = None,
        **kwargs,
    ) -> Dict[str, Any]:
        """Store a memory observation."""
        resolved_agent_id = agent_id or self.agent_id
        payload: Dict[str, Any] = {
            "observation": observation,
            "tags": tags or [],
            "importance": importance,
        }
        if resolved_agent_id:
            payload["agent_id"] = resolved_agent_id
        if space_id:
            payload["space_id"] = space_id
        if ttl_seconds is not None:
            payload["ttl_seconds"] = ttl_seconds
        if auto_link:
            payload["auto_link"] = True
        if context:
            payload["context"] = context
        elif metadata:
            payload["context"] = _json.dumps(metadata)

        params = {}
        if conflict_strategy:
            params["conflict_strategy"] = conflict_strategy

        return await self._request("POST", "/v1/memories", params=params or None, json_data=payload)

    async def recall(
        self,
        query: str,
        *,
        limit: int = 5,
        tags: Optional[List[str]] = None,
        min_score: float = 0.0,
        scope: str = "own",
        agents: Optional[List[str]] = None,
        include_shared: bool = False,
        space_id: Optional[str] = None,
        recency_weight: float = 0.0,
        include_superseded: bool = False,
        **kwargs,
    ) -> SearchResult:
        """Search memories semantically."""
        params: Dict[str, Any] = {"q": query, "limit": limit, "min_score": min_score}
        if tags:
            params["tags"] = ",".join(tags)
        if agents:
            params["agents"] = ",".join(agents)
        if space_id:
            params["space_id"] = space_id
        if recency_weight > 0:
            params["recency_weight"] = recency_weight
        if include_superseded:
            params["include_superseded"] = "true"

        result = await self._request("GET", "/v1/memories/search", params=params)
        raw = result if isinstance(result, list) else result.get("memories", [])
        memories = [Memory.from_dict(m) for m in raw]
        return SearchResult(query=query, total_results=len(memories), memories=memories)

    async def draft_memory(
        self,
        observation: str,
        *,
        tags: Optional[List[str]] = None,
        context: Optional[str] = None,
        importance: int = 5,
        confidence: float = 1.0,
        agent_id: Optional[str] = None,
        space_id: Optional[str] = None,
        ttl_seconds: Optional[int] = None,
        branch_id: Optional[str] = None,
        **kwargs,
    ) -> Dict[str, Any]:
        """Create a reviewable memory draft without writing to canonical memory."""
        resolved_agent_id = agent_id or self.agent_id
        payload: Dict[str, Any] = {
            "observation": observation,
            "tags": tags or [],
            "importance": importance,
            "confidence": confidence,
        }
        if resolved_agent_id:
            payload["agent_id"] = resolved_agent_id
        if context:
            payload["context"] = context
        if space_id:
            payload["space_id"] = space_id
        if ttl_seconds is not None:
            payload["ttl_seconds"] = ttl_seconds
        if branch_id:
            payload["branch_id"] = branch_id
        return await self._request("POST", "/v1/memory-drafts", json_data=payload)

    async def memory_drafts(self, *, status: Optional[str] = None, branch_id: Optional[str] = None) -> Dict[str, Any]:
        """List memory drafts for the current tenant."""
        params: Dict[str, Any] = {}
        if status:
            params["status"] = status
        if branch_id:
            params["branch_id"] = branch_id
        return await self._request("GET", "/v1/memory-drafts", params=params)

    async def memory_draft(self, draft_id: str) -> Dict[str, Any]:
        """Fetch a single memory draft."""
        return await self._request("GET", f"/v1/memory-drafts/{draft_id}")

    async def draft_diff(self, draft_id: str, *, compare_to: Optional[str] = None) -> Dict[str, Any]:
        """Get a field-level diff between a draft and existing memory."""
        params = {"compare_to": compare_to} if compare_to else None
        return await self._request("GET", f"/v1/memory-drafts/{draft_id}/diff", params=params)

    async def merge_draft(
        self,
        draft_id: str,
        *,
        supersede_memory_id: Optional[str] = None,
    ) -> Dict[str, Any]:
        """Merge a reviewed draft into canonical memory."""
        payload = {"supersede_memory_id": supersede_memory_id} if supersede_memory_id else {}
        return await self._request("POST", f"/v1/memory-drafts/{draft_id}/merge", json_data=payload)

    async def reject_draft(self, draft_id: str, *, reason: Optional[str] = None) -> Dict[str, Any]:
        """Reject a memory draft."""
        payload = {"reason": reason} if reason else {}
        return await self._request("POST", f"/v1/memory-drafts/{draft_id}/reject", json_data=payload)

    async def memory_branch(self, branch_id: str) -> Dict[str, Any]:
        """Get grouped review information for a draft branch/session."""
        return await self._request("GET", f"/v1/memory-branches/{branch_id}")

    async def merge_branch(self, branch_id: str) -> Dict[str, Any]:
        """Merge all open drafts in a branch/session."""
        return await self._request("POST", f"/v1/memory-branches/{branch_id}/merge", json_data={})

    async def reject_branch(self, branch_id: str, *, reason: Optional[str] = None) -> Dict[str, Any]:
        """Reject all open drafts in a branch/session."""
        payload = {"reason": reason} if reason else {}
        return await self._request("POST", f"/v1/memory-branches/{branch_id}/reject", json_data=payload)

    async def memories(
        self,
        *,
        limit: int = 100,
        offset: int = 0,
        tags: Optional[List[str]] = None,
        min_importance: Optional[int] = None,
        include_shared: bool = False,
        agent_id: Optional[str] = None,
        space_id: Optional[str] = None,
        **kwargs,
    ) -> List[Dict[str, Any]]:
        """List all memories with optional filtering."""
        params: Dict[str, Any] = {"limit": limit, "offset": offset}
        if tags:
            params["tags"] = ",".join(tags)
        if min_importance is not None:
            params["min_importance"] = min_importance
        if include_shared:
            params["include_shared"] = "true"
        if agent_id:
            params["agent_id"] = agent_id
        if space_id:
            params["space_id"] = space_id
        result = await self._request("GET", "/v1/memories", params=params)
        return result.get("memories", []) if isinstance(result, dict) else result

    async def memory(self, memory_id: str) -> Dict[str, Any]:
        """Get a specific memory by ID."""
        return await self._request("GET", f"/v1/memories/{memory_id}")

    async def forget(self, memory_id: str) -> bool:
        """Delete a specific memory."""
        result = await self._request("DELETE", f"/v1/memories/{memory_id}")
        return result.get("success", True)

    async def supersede(self, old_memory_id: str, new_memory_id: str) -> Dict[str, Any]:
        """Mark a memory as superseded by another."""
        return await self._request("PATCH", f"/v1/memories/{old_memory_id}", json_data={"superseded_by": new_memory_id})

    async def list(
        self,
        *,
        limit: int = 100,
        offset: int = 0,
        tags: Optional[List[str]] = None,
        min_importance: Optional[int] = None,
        include_shared: bool = False,
        agent_id: Optional[str] = None,
        space_id: Optional[str] = None,
        **kwargs,
    ) -> ListResult:
        """List memories with rich return type."""
        params: Dict[str, Any] = {"limit": limit, "offset": offset}
        if tags:
            params["tags"] = ",".join(tags)
        if min_importance is not None:
            params["min_importance"] = min_importance
        if include_shared:
            params["include_shared"] = "true"
        if agent_id:
            params["agent_id"] = agent_id
        if space_id:
            params["space_id"] = space_id
        result = await self._request("GET", "/v1/memories", params=params)
        raw = result.get("memories", []) if isinstance(result, dict) else result
        return ListResult(
            total_count=result.get("total_count", len(raw)) if isinstance(result, dict) else len(raw),
            memories=[Memory.from_dict(m) for m in raw],
            filters=result.get("filters", {}) if isinstance(result, dict) else {},
        )

    async def delete(self, memory_id: str) -> bool:
        """Delete a specific memory. Alias for forget()."""
        return await self.forget(memory_id)

    async def stats(self) -> Dict[str, Any]:
        """Get memory statistics."""
        return await self._request("GET", "/v1/memories/stats")

    # ── Context Spaces ────────────────────────────────────────────────

    async def create_space(
        self,
        name: str,
        *,
        description: Optional[str] = None,
        allowed_agent_ids: Optional[List[str]] = None,
        allowed_tenant_ids: Optional[List[str]] = None,
        tags: Optional[List[str]] = None,
    ) -> Dict[str, Any]:
        """Create a context space for multi-agent collaboration."""
        body: Dict[str, Any] = {"name": name}
        if description is not None:
            body["description"] = description
        if allowed_agent_ids is not None:
            body["allowed_agent_ids"] = allowed_agent_ids
        if allowed_tenant_ids is not None:
            body["allowed_tenant_ids"] = allowed_tenant_ids
        if tags is not None:
            body["tags"] = tags
        return await self._request("POST", "/v1/context-spaces", json_data=body)

    async def list_spaces(self) -> Dict[str, Any]:
        """List all context spaces."""
        return await self._request("GET", "/v1/context-spaces")

    async def get_space(self, space_id: str) -> Dict[str, Any]:
        """Get a context space by ID."""
        return await self._request("GET", f"/v1/context-spaces/{space_id}")

    async def update_space(self, space_id: str, *, name: Optional[str] = None, description: Optional[str] = None, allowed_agent_ids: Optional[List[str]] = None, tags: Optional[List[str]] = None) -> Dict[str, Any]:
        """Update a context space."""
        body: Dict[str, Any] = {}
        if name is not None:
            body["name"] = name
        if description is not None:
            body["description"] = description
        if allowed_agent_ids is not None:
            body["allowed_agent_ids"] = allowed_agent_ids
        if tags is not None:
            body["tags"] = tags
        return await self._request("PUT", f"/v1/context-spaces/{space_id}", json_data=body)

    async def delete_space(self, space_id: str) -> Dict[str, Any]:
        """Delete a context space."""
        return await self._request("DELETE", f"/v1/context-spaces/{space_id}")

    async def space_memories(self, space_id: str, *, query: Optional[str] = None, tags: Optional[List[str]] = None, agent_id: Optional[str] = None, limit: int = 100, offset: int = 0) -> Dict[str, Any]:
        """List or search memories within a context space."""
        params: Dict[str, Any] = {"limit": limit, "offset": offset}
        if query is not None:
            params["q"] = query
        if tags is not None:
            params["tags"] = ",".join(tags)
        if agent_id is not None:
            params["agent_id"] = agent_id
        return await self._request("GET", f"/v1/context-spaces/{space_id}/memories", params=params)

    # ── Sharing ───────────────────────────────────────────────────────

    async def share_context(self, tag: str, to_email: str, permission: str = "read") -> Dict[str, Any]:
        """Share a tag/space with another user via email."""
        return await self._request("POST", "/v1/sharing/share", json_data={"tag": tag, "to_email": to_email, "permission": permission})

    async def accept_shared_context(self, token: str) -> Dict[str, Any]:
        """Accept a shared context invitation."""
        return await self._request("POST", "/v1/sharing/accept", json_data={"token": token})

    async def shared_contexts(self) -> Dict[str, Any]:
        """List shared contexts."""
        return await self._request("GET", "/v1/sharing/shared")

    async def revoke_shared_context(self, token: str) -> Dict[str, Any]:
        """Revoke a shared context."""
        return await self._request("DELETE", f"/v1/sharing/revoke/{token}")

    async def graph(self, *, memory_id: Optional[str] = None, tag: Optional[str] = None, depth: int = 2, limit: int = 100) -> Dict[str, Any]:
        """Get memory graph (nodes and edges)."""
        params: Dict[str, Any] = {"depth": depth, "limit": limit}
        if memory_id:
            params["memory_id"] = memory_id
        if tag:
            params["tag"] = tag
        return await self._request("GET", "/v1/memories/graph", params=params)

    # ── Rollback ──────────────────────────────────────────────────────

    async def rollback(
        self,
        target: str,
        *,
        dry_run: bool = False,
        reason: Optional[str] = None,
        tags: Optional[List[str]] = None,
        agent_id: Optional[str] = None,
        strategy: str = "restore",
    ) -> Dict[str, Any]:
        """Execute a rollback to undo memory operations."""
        payload: Dict[str, Any] = {"target_timestamp": target, "strategy": strategy}
        if dry_run:
            payload["dry_run"] = True
        if reason:
            payload["reason"] = reason
        if tags:
            payload["tags"] = tags
        if agent_id:
            payload["agent_id"] = agent_id
        return await self._request("POST", "/v1/rollback/rollback", json_data=payload)

    async def rollback_preview(self, target: str) -> Dict[str, Any]:
        """Preview a rollback without executing it."""
        return await self._request("POST", "/v1/rollback/rollback", json_data={"target_timestamp": target, "dry_run": True})

    async def rollback_history(self, limit: int = 50) -> List[Dict[str, Any]]:
        """Get rollback history."""
        result = await self._request("GET", "/v1/rollback/history", params={"limit": limit})
        return result.get("rollbacks", []) if isinstance(result, dict) else result

    # ── Audit ─────────────────────────────────────────────────────────

    async def audit(self, limit: int = 50, since: Optional[str] = None, until: Optional[str] = None, operation: Optional[str] = None, artifact_id: Optional[str] = None) -> List[Dict[str, Any]]:
        """Get audit trail entries."""
        params: Dict[str, Any] = {"limit": limit}
        if since:
            params["start_time"] = since
        if until:
            params["end_time"] = until
        if operation:
            params["operation"] = operation
        if artifact_id:
            params["artifact_id"] = artifact_id
        result = await self._request("GET", "/v1/audit", params=params)
        return result.get("entries", []) if isinstance(result, dict) else result

    async def audit_export(self, format: str = "csv") -> bytes:
        """Export audit log."""
        response = await self._request("GET", "/v1/audit/export", params={"format": format}, raw_response=True)
        return response.content

    async def audit_verify(self) -> Dict[str, Any]:
        """Verify audit chain integrity."""
        return await self._request("GET", "/v1/audit/verify")

    # ── Traces ────────────────────────────────────────────────────────

    async def trace_create(self, agent_id: str, *, name: Optional[str] = None, metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
        """Create a new trace session."""
        payload: Dict[str, Any] = {"agent_id": agent_id}
        if name:
            payload["name"] = name
        if metadata:
            payload["metadata"] = metadata
        return await self._request("POST", "/v1/traces", json_data=payload)

    async def trace_step(self, trace_id: str, step_type: str, content: Dict[str, Any], *, metadata: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
        """Add a step to a trace."""
        payload: Dict[str, Any] = {"step_type": step_type, "content": content}
        if metadata:
            payload["metadata"] = metadata
        return await self._request("POST", f"/v1/traces/{trace_id}/steps", json_data=payload)

    async def trace_complete(self, trace_id: str) -> Dict[str, Any]:
        """Finalize a trace with RSA signature."""
        return await self._request("POST", f"/v1/traces/{trace_id}/complete")

    async def trace_verify(self, trace_id: str) -> Dict[str, Any]:
        """Verify a trace's integrity."""
        return await self._request("POST", f"/v1/traces/{trace_id}/verify")

    # ── Usage & System ────────────────────────────────────────────────

    async def usage(self) -> Dict[str, Any]:
        """Get current usage vs limits."""
        return await self._request("GET", "/v1/usage")

    async def plans(self) -> List[Dict[str, Any]]:
        """Get available pricing plans."""
        result = await self._request("GET", "/v1/plans")
        return result.get("plans", []) if isinstance(result, dict) else result

    async def health(self) -> Dict[str, Any]:
        """Check API health."""
        return await self._request("GET", "/health")

    async def context_now(self) -> Dict[str, Any]:
        """Get temporal context snapshot."""
        return await self._request("GET", "/v1/context/now")

    # ── Links / Edges ─────────────────────────────────────────────────

    async def link(self, source_id: str, target_id: str, *, relation: str = "related_to", weight: float = 1.0) -> Dict[str, Any]:
        """Create a directed edge between memories."""
        return await self._request("POST", "/v1/memories/link", json_data={"source_id": source_id, "target_id": target_id, "relation": relation, "weight": weight})

    async def unlink(self, source_id: str, target_id: str, *, relation: Optional[str] = None) -> Dict[str, Any]:
        """Remove edge(s) between memories."""
        body: Dict[str, Any] = {"source_id": source_id, "target_id": target_id}
        if relation:
            body["relation"] = relation
        return await self._request("DELETE", "/v1/memories/link", json_data=body)

    async def links(self, memory_id: str, *, relation: Optional[str] = None) -> Dict[str, Any]:
        """Get edges for a memory."""
        params: Dict[str, Any] = {}
        if relation:
            params["relation"] = relation
        return await self._request("GET", f"/v1/memories/{memory_id}/links", params=params or None)

    async def edges(self, *, source_id: Optional[str] = None, target_id: Optional[str] = None, relation: Optional[str] = None, limit: int = 100, offset: int = 0) -> Dict[str, Any]:
        """List all edges with filtering."""
        params: Dict[str, Any] = {"limit": limit, "offset": offset}
        if source_id:
            params["source_id"] = source_id
        if target_id:
            params["target_id"] = target_id
        if relation:
            params["relation"] = relation
        return await self._request("GET", "/v1/memories/edges", params=params)

    # ── Knowledge Graph ───────────────────────────────────────────────

    async def triple(self, subject: str, predicate: str, object_val: str, *, confidence: float = 1.0, source_memory_id: Optional[str] = None) -> Dict[str, Any]:
        """Create a knowledge graph triple."""
        payload: Dict[str, Any] = {"subject": subject, "predicate": predicate, "object": object_val, "confidence": confidence}
        if source_memory_id:
            payload["source_memory_id"] = source_memory_id
        return await self._request("POST", "/v1/knowledge-graph/triples", json_data=payload)

    async def triples(self, *, subject: Optional[str] = None, predicate: Optional[str] = None, object_val: Optional[str] = None, limit: int = 100, offset: int = 0) -> Dict[str, Any]:
        """List triples with filters."""
        params: Dict[str, Any] = {"limit": limit, "offset": offset}
        if subject:
            params["subject"] = subject
        if predicate:
            params["predicate"] = predicate
        if object_val:
            params["object"] = object_val
        return await self._request("GET", "/v1/knowledge-graph/triples", params=params)

    async def delete_triple(self, triple_id: str) -> Dict[str, Any]:
        """Delete a triple."""
        return await self._request("DELETE", f"/v1/knowledge-graph/triples/{triple_id}")

    async def entities(self, *, limit: int = 100, offset: int = 0, search: Optional[str] = None) -> Dict[str, Any]:
        """List entities."""
        params: Dict[str, Any] = {"limit": limit, "offset": offset}
        if search:
            params["search"] = search
        return await self._request("GET", "/v1/knowledge-graph/entities", params=params)

    async def entity(self, entity_id: str) -> Dict[str, Any]:
        """Get entity and traversal."""
        return await self._request("GET", f"/v1/knowledge-graph/entities/{entity_id}")

    async def delete_entity(self, entity_id: str) -> Dict[str, Any]:
        """Delete entity (cascade)."""
        return await self._request("DELETE", f"/v1/knowledge-graph/entities/{entity_id}")

    # ── Replay ────────────────────────────────────────────────────────

    async def replay_timeline(self, *, since: Optional[str] = None, until: Optional[str] = None, operations: Optional[List[str]] = None, agent_id: Optional[str] = None, limit: int = 100, offset: int = 0) -> Dict[str, Any]:
        """Get operation timeline."""
        params: Dict[str, Any] = {"limit": limit, "offset": offset}
        if since:
            params["since"] = since
        if until:
            params["until"] = until
        if operations:
            params["operations"] = ",".join(operations)
        if agent_id:
            params["agent_id"] = agent_id
        return await self._request("GET", "/v1/replay/timeline", params=params)

    async def replay_snapshot(self, at: str, *, limit: int = 500, offset: int = 0) -> Dict[str, Any]:
        """Reconstruct memory state at a timestamp."""
        return await self._request("GET", "/v1/replay/snapshot", params={"at": at, "limit": limit, "offset": offset})

    async def replay_memory(self, memory_id: str) -> Dict[str, Any]:
        """Get full lifecycle of a memory."""
        return await self._request("GET", f"/v1/replay/memory/{memory_id}")

    async def replay_recall(self, query: str, at: str, *, limit: int = 5) -> Dict[str, Any]:
        """Counterfactual recall — what would be recalled at a given time."""
        return await self._request("POST", "/v1/replay/recall", json_data={"query": query, "at": at, "limit": limit})

    async def replay_diff(self, from_ts: str, to_ts: str) -> Dict[str, Any]:
        """Memory diff between two timestamps."""
        return await self._request("GET", "/v1/replay/diff", params={"from": from_ts, "to": to_ts})

    async def replay_drift(self, from_ts: str, to_ts: str) -> Dict[str, Any]:
        """Memory composition drift analysis."""
        return await self._request("GET", "/v1/replay/drift", params={"from": from_ts, "to": to_ts})

    # ── Cortex ────────────────────────────────────────────────────────

    async def cortex_status(self) -> Dict[str, Any]:
        """Get Cortex status."""
        return await self._request("GET", "/v1/cortex/status")

    async def cortex_config(self) -> Dict[str, Any]:
        """Get Cortex configuration."""
        return await self._request("GET", "/v1/cortex/config")

    async def cortex_update_config(self, **kwargs) -> Dict[str, Any]:
        """Update Cortex configuration."""
        return await self._request("PATCH", "/v1/cortex/config", json_data=kwargs)

    async def cortex_run(self) -> Dict[str, Any]:
        """Trigger a manual Cortex cycle."""
        return await self._request("POST", "/v1/cortex/run")

    async def cortex_insights(self, *, limit: int = 20, offset: int = 0) -> Dict[str, Any]:
        """Get Cortex-generated insights."""
        return await self._request("GET", "/v1/cortex/insights", params={"limit": limit, "offset": offset})

    # ── Eval ──────────────────────────────────────────────────────────

    async def eval_run(self, *, min_score: Optional[float] = None) -> Dict[str, Any]:
        """
        Run a memory health evaluation. Returns health_score (0-100) with breakdown.
        Free: 3/day, Starter: 30/day, Pro: unlimited.
        """
        payload = {}
        if min_score is not None:
            payload["min_score"] = min_score
        return await self._request("POST", "/v1/eval/run", json_data=payload or None)

    async def eval_gate(self, min_score: float) -> Dict[str, Any]:
        """CI/CD quality gate. Returns result if passed, raises on failure. Pro required."""
        return await self._request("POST", "/v1/eval/gate", json_data={"min_score": min_score})

    async def eval_history(self, *, limit: int = 50, offset: int = 0) -> Dict[str, Any]:
        """Get eval score history."""
        return await self._request("GET", "/v1/eval/history", params={"limit": limit, "offset": offset})

    async def eval_drift(self, *, days: int = 7) -> Dict[str, Any]:
        """Get memory drift analysis. Starter+."""
        return await self._request("GET", "/v1/eval/drift", params={"days": days})

    async def eval_baseline_create(self, query: str, expected_observation: str) -> Dict[str, Any]:
        """Save a recall baseline for regression testing."""
        return await self._request("POST", "/v1/eval/baselines", json_data={"query": query, "expected_observation": expected_observation})

    async def eval_baselines(self) -> Dict[str, Any]:
        """List all saved eval baselines."""
        return await self._request("GET", "/v1/eval/baselines")

    async def eval_baseline_delete(self, baseline_id: str) -> bool:
        """Delete an eval baseline."""
        await self._request("DELETE", f"/v1/eval/baselines/{baseline_id}")
        return True

    # =========================================================================
    # Sentinel Intel — Threat Intelligence (Pro+)
    # =========================================================================

    async def threat_feed(self, *, hours: int = 24, min_severity: str = "medium") -> Dict[str, Any]:
        """Get anonymized threat intelligence feed."""
        return await self._request("GET", "/v1/sentinel-intel/threats/feed", params={"hours": hours, "min_severity": min_severity})

    async def threat_stats(self) -> Dict[str, Any]:
        """Get threat intelligence statistics."""
        return await self._request("GET", "/v1/sentinel-intel/threats/stats")

    async def threat_record(self, threat_event: Dict[str, Any]) -> Dict[str, Any]:
        """Record a threat event."""
        return await self._request("POST", "/v1/sentinel-intel/threats/record", json_data={"threat_event": threat_event})

    async def threat_trending(self, *, hours: int = 24, min_occurrences: int = 2) -> Dict[str, Any]:
        """Get trending threat signatures."""
        return await self._request("GET", "/v1/sentinel-intel/threats/trending", params={"hours": hours, "min_occurrences": min_occurrences})

    async def threat_match(self, threat_event: Dict[str, Any], *, min_similarity: float = 0.8) -> Dict[str, Any]:
        """Find matching threat signatures."""
        return await self._request("POST", "/v1/sentinel-intel/threats/match", json_data={"threat_event": threat_event, "min_similarity": min_similarity})

    async def threat_signature(self, signature_id: str) -> Dict[str, Any]:
        """Get a specific threat signature."""
        return await self._request("GET", f"/v1/sentinel-intel/threats/{signature_id}")

    async def threat_mitigate(self, signature_id: str) -> Dict[str, Any]:
        """Mark a threat signature as mitigated."""
        return await self._request("POST", f"/v1/sentinel-intel/threats/{signature_id}/mitigate")

    # =========================================================================
    # Sentinel Intel — Auto Defense (Pro+)
    # =========================================================================

    async def defense_list(self, *, rule_type: Optional[str] = None) -> Dict[str, Any]:
        """List active defense rules."""
        params = {}
        if rule_type:
            params["rule_type"] = rule_type
        return await self._request("GET", "/v1/sentinel-intel/defenses", params=params or None)

    async def defense_deploy(self, signature_id: str, rule_type: str, rule_config: Optional[Dict] = None) -> Dict[str, Any]:
        """Deploy a defense rule."""
        body: Dict[str, Any] = {"signature_id": signature_id, "rule_type": rule_type}
        if rule_config:
            body["rule_config"] = rule_config
        return await self._request("POST", "/v1/sentinel-intel/defenses/deploy", json_data=body)

    async def defense_remove(self, defense_id: str) -> Dict[str, Any]:
        """Remove a defense rule."""
        return await self._request("DELETE", f"/v1/sentinel-intel/defenses/{defense_id}")

    async def defense_effectiveness(self, defense_id: str) -> Dict[str, Any]:
        """Measure defense effectiveness."""
        return await self._request("GET", f"/v1/sentinel-intel/defenses/{defense_id}/effectiveness")

    async def defense_record_block(self, defense_id: str, *, is_false_positive: bool = False) -> Dict[str, Any]:
        """Record a defense block event."""
        return await self._request("POST", f"/v1/sentinel-intel/defenses/{defense_id}/block", json_data={"is_false_positive": is_false_positive})

    async def defense_stats(self) -> Dict[str, Any]:
        """Get defense statistics."""
        return await self._request("GET", "/v1/sentinel-intel/defenses/stats")

    async def defense_recommend(self, signature_id: str) -> Dict[str, Any]:
        """Get defense recommendation for a signature."""
        return await self._request("GET", f"/v1/sentinel-intel/defenses/recommend/{signature_id}")

    # =========================================================================
    # Sentinel Intel — Correlation (Pro+)
    # =========================================================================

    async def correlate_threat(self, threat_event: Dict[str, Any]) -> Dict[str, Any]:
        """Correlate a threat across tenants."""
        return await self._request("POST", "/v1/sentinel-intel/correlation/correlate", json_data={"threat_event": threat_event})

    async def detect_campaign(self, *, hours: int = 24) -> Dict[str, Any]:
        """Detect attack campaigns."""
        return await self._request("GET", "/v1/sentinel-intel/correlation/campaign", params={"hours": hours})

    async def coordinated_attack_check(self, threat_events: list, *, time_window_hours: Optional[int] = None) -> Dict[str, Any]:
        """Check for coordinated attacks."""
        body: Dict[str, Any] = {"threat_events": threat_events}
        if time_window_hours is not None:
            body["time_window_hours"] = time_window_hours
        return await self._request("POST", "/v1/sentinel-intel/correlation/coordinated", json_data=body)

    async def related_signatures(self, signature_id: str, *, similarity_threshold: float = 0.7) -> Dict[str, Any]:
        """Find related threat signatures."""
        return await self._request("GET", f"/v1/sentinel-intel/correlation/related/{signature_id}", params={"similarity_threshold": similarity_threshold})

    # =========================================================================
    # Streams
    # =========================================================================

    async def stream_status(self) -> Dict[str, Any]:
        """Get memory stream connection status."""
        return await self._request("GET", "/v1/streams/status")

    # =========================================================================
    # Agent Methods (Runtime v2)
    # =========================================================================

    async def create_agent(
        self,
        name: str,
        *,
        agent_id: Optional[str] = None,
        description: Optional[str] = None,
        model: str = "gpt-4o-mini",
        provider: str = "openai",
        instructions: Optional[str] = None,
        memory_scope: Optional[str] = None,
        capabilities: Optional[List[str]] = None,
        policy_profile: Optional[Dict[str, Any]] = None,
        config: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """Create a first-class agent entity."""
        payload: Dict[str, Any] = {"name": name, "model": model, "provider": provider}
        if agent_id:
            payload["agent_id"] = agent_id
        if description:
            payload["description"] = description
        if instructions:
            payload["instructions"] = instructions
        if memory_scope:
            payload["memory_scope"] = memory_scope
        if capabilities:
            payload["capabilities"] = capabilities
        if policy_profile:
            payload["policy_profile"] = policy_profile
        if config:
            payload["config"] = config
        return await self._request("POST", "/v1/agents", json_data=payload)

    async def get_agent(self, agent_id: str) -> Dict[str, Any]:
        """Get an agent by ID."""
        return await self._request("GET", f"/v1/agents/{agent_id}")

    async def list_agents(self, *, status: Optional[str] = None, limit: int = 100, offset: int = 0) -> Dict[str, Any]:
        """List all agents for this tenant."""
        params: Dict[str, Any] = {"limit": limit, "offset": offset}
        if status:
            params["status"] = status
        return await self._request("GET", "/v1/agents", params=params)

    async def update_agent(self, agent_id: str, **kwargs) -> Dict[str, Any]:
        """Update agent fields. Pass only the fields to change."""
        return await self._request("PATCH", f"/v1/agents/{agent_id}", json_data=kwargs)

    async def delete_agent(self, agent_id: str) -> Dict[str, Any]:
        """Delete an agent."""
        return await self._request("DELETE", f"/v1/agents/{agent_id}")

    # =========================================================================
    # Mission Methods (Runtime v2)
    # =========================================================================

    async def create_mission(
        self,
        agent_id: str,
        goal: str,
        *,
        constraints: Optional[List[str]] = None,
        success_criteria: Optional[List[str]] = None,
        allowed_capabilities: Optional[List[str]] = None,
        escalation_rules: Optional[List[Dict[str, Any]]] = None,
        stop_conditions: Optional[List[str]] = None,
        config: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """Create a mission for an agent. Returns the mission record.

        Example:
            >>> mission = await nx.create_mission("agent-uuid", "Research competitor pricing",
            ...     constraints=["no external APIs"], success_criteria=["report generated"])
            >>> print(mission["mission_id"])
        """
        payload: Dict[str, Any] = {"agent_id": agent_id, "goal": goal}
        if constraints is not None:
            payload["constraints"] = constraints
        if success_criteria is not None:
            payload["success_criteria"] = success_criteria
        if allowed_capabilities is not None:
            payload["allowed_capabilities"] = allowed_capabilities
        if escalation_rules is not None:
            payload["escalation_rules"] = escalation_rules
        if stop_conditions is not None:
            payload["stop_conditions"] = stop_conditions
        if config is not None:
            payload["config"] = config
        return await self._request("POST", "/v1/missions", json_data=payload)

    async def get_mission(self, mission_id: str) -> Dict[str, Any]:
        """Get a mission by ID.

        Example:
            >>> mission = await nx.get_mission("mission-uuid-here")
            >>> print(mission["goal"], mission["status"])
        """
        return await self._request("GET", f"/v1/missions/{mission_id}")

    async def list_missions(
        self,
        *,
        agent_id: Optional[str] = None,
        status: Optional[str] = None,
        limit: int = 100,
        offset: int = 0,
    ) -> Dict[str, Any]:
        """List missions, optionally filtered by agent or status.

        Example:
            >>> result = await nx.list_missions(agent_id="agent-uuid", status="active")
            >>> for m in result["missions"]:
            ...     print(m["goal"], m["status"])
        """
        params: Dict[str, Any] = {"limit": limit, "offset": offset}
        if agent_id:
            params["agent_id"] = agent_id
        if status:
            params["status"] = status
        return await self._request("GET", "/v1/missions", params=params)

    async def update_mission(self, mission_id: str, **kwargs) -> Dict[str, Any]:
        """Update mission fields. Pass only the fields to change.

        Example:
            >>> await nx.update_mission("mission-uuid", goal="Updated goal")
        """
        return await self._request("PATCH", f"/v1/missions/{mission_id}", json_data=kwargs)

    async def delete_mission(self, mission_id: str) -> Dict[str, Any]:
        """Delete a mission.

        Example:
            >>> await nx.delete_mission("mission-uuid-here")
        """
        return await self._request("DELETE", f"/v1/missions/{mission_id}")

    async def pause_mission(self, mission_id: str) -> Dict[str, Any]:
        """Pause an active mission.

        Example:
            >>> await nx.pause_mission("mission-uuid-here")
        """
        return await self._request("POST", f"/v1/missions/{mission_id}/pause")

    async def resume_mission(self, mission_id: str) -> Dict[str, Any]:
        """Resume a paused mission.

        Example:
            >>> await nx.resume_mission("mission-uuid-here")
        """
        return await self._request("POST", f"/v1/missions/{mission_id}/resume")

    async def cancel_mission(self, mission_id: str) -> Dict[str, Any]:
        """Cancel a mission.

        Example:
            >>> await nx.cancel_mission("mission-uuid-here")
        """
        return await self._request("POST", f"/v1/missions/{mission_id}/cancel")

    # =========================================================================
    # Capability Methods (Runtime v2)
    # =========================================================================

    async def create_capability(
        self,
        name: str,
        *,
        capability_id: Optional[str] = None,
        description: Optional[str] = None,
        tools: Optional[List[Dict[str, Any]]] = None,
        risk_levels: Optional[Dict[str, Any]] = None,
        approval_requirements: Optional[Dict[str, Any]] = None,
        memory_behavior: Optional[Dict[str, Any]] = None,
        eval_rules: Optional[Dict[str, Any]] = None,
        config: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """Create a capability pack. Returns the capability record.

        Example:
            >>> cap = await nx.create_capability("web-search",
            ...     description="Search the web", tools=[{"name": "google"}])
            >>> print(cap["capability_id"])
        """
        payload: Dict[str, Any] = {"name": name}
        if capability_id is not None:
            payload["capability_id"] = capability_id
        if description is not None:
            payload["description"] = description
        if tools is not None:
            payload["tools"] = tools
        if risk_levels is not None:
            payload["risk_levels"] = risk_levels
        if approval_requirements is not None:
            payload["approval_requirements"] = approval_requirements
        if memory_behavior is not None:
            payload["memory_behavior"] = memory_behavior
        if eval_rules is not None:
            payload["eval_rules"] = eval_rules
        if config is not None:
            payload["config"] = config
        return await self._request("POST", "/v1/capabilities", json_data=payload)

    async def get_capability(self, capability_id: str) -> Dict[str, Any]:
        """Get a capability by ID.

        Example:
            >>> cap = await nx.get_capability("cap-uuid-here")
            >>> print(cap["name"], cap["status"])
        """
        return await self._request("GET", f"/v1/capabilities/{capability_id}")

    async def list_capabilities(
        self,
        *,
        status: Optional[str] = None,
        limit: int = 100,
        offset: int = 0,
    ) -> Dict[str, Any]:
        """List capabilities, optionally filtered by status.

        Example:
            >>> result = await nx.list_capabilities(status="active")
            >>> for c in result["capabilities"]:
            ...     print(c["name"], c["status"])
        """
        params: Dict[str, Any] = {"limit": limit, "offset": offset}
        if status:
            params["status"] = status
        return await self._request("GET", "/v1/capabilities", params=params)

    async def update_capability(self, capability_id: str, **kwargs) -> Dict[str, Any]:
        """Update capability fields. Pass only the fields to change.

        Example:
            >>> await nx.update_capability("cap-uuid", description="Updated desc")
        """
        return await self._request(
            "PATCH", f"/v1/capabilities/{capability_id}", json_data=kwargs
        )

    async def delete_capability(self, capability_id: str) -> Dict[str, Any]:
        """Delete a capability.

        Example:
            >>> await nx.delete_capability("cap-uuid-here")
        """
        return await self._request("DELETE", f"/v1/capabilities/{capability_id}")

    # =========================================================================
    # Checkpoint Methods (Runtime v2)
    # =========================================================================

    async def create_checkpoint(
        self,
        mission_id: str,
        *,
        label: Optional[str] = None,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """Create a checkpoint for a mission.

        Example:
            >>> ckpt = await nx.create_checkpoint("msn_abc123", label="before-deploy")
            >>> print(ckpt["checkpoint_id"])
        """
        payload: Dict[str, Any] = {}
        if label is not None:
            payload["label"] = label
        if metadata is not None:
            payload["metadata"] = metadata
        return await self._request(
            "POST", f"/v1/missions/{mission_id}/checkpoints", json_data=payload
        )

    async def list_checkpoints(
        self,
        mission_id: str,
        *,
        limit: int = 100,
        offset: int = 0,
    ) -> Dict[str, Any]:
        """List checkpoints for a mission.

        Example:
            >>> result = await nx.list_checkpoints("msn_abc123")
            >>> for c in result["checkpoints"]:
            ...     print(c["checkpoint_id"], c["label"])
        """
        params: Dict[str, Any] = {"limit": limit, "offset": offset}
        return await self._request(
            "GET", f"/v1/missions/{mission_id}/checkpoints", params=params
        )

    async def get_checkpoint(self, checkpoint_id: str) -> Dict[str, Any]:
        """Get a single checkpoint by ID.

        Example:
            >>> ckpt = await nx.get_checkpoint("ckpt_abc123")
            >>> print(ckpt["mission_status"])
        """
        return await self._request("GET", f"/v1/checkpoints/{checkpoint_id}")

    async def rollback_to_checkpoint(
        self,
        mission_id: str,
        checkpoint_id: str,
        *,
        reason: Optional[str] = None,
    ) -> Dict[str, Any]:
        """Rollback a mission to a specific checkpoint.

        Example:
            >>> result = await nx.rollback_to_checkpoint("msn_abc123", "ckpt_abc123",
            ...     reason="bad deploy")
        """
        payload: Dict[str, Any] = {"checkpoint_id": checkpoint_id}
        if reason is not None:
            payload["reason"] = reason
        return await self._request(
            "POST", f"/v1/missions/{mission_id}/rollback", json_data=payload
        )

    # =========================================================================
    # Intervention Methods (Runtime v2)
    # =========================================================================

    async def create_intervention(
        self,
        intervention_type: str,
        *,
        mission_id: Optional[str] = None,
        action_id: Optional[str] = None,
        agent_id: Optional[str] = None,
        rationale: Optional[str] = None,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """Record a supervisor intervention.

        Example:
            >>> intv = await nx.create_intervention("pause",
            ...     mission_id="mis_abc", rationale="Needs review")
            >>> print(intv["intervention_id"])
        """
        payload: Dict[str, Any] = {"intervention_type": intervention_type}
        if mission_id is not None:
            payload["mission_id"] = mission_id
        if action_id is not None:
            payload["action_id"] = action_id
        if agent_id is not None:
            payload["agent_id"] = agent_id
        if rationale is not None:
            payload["rationale"] = rationale
        if metadata is not None:
            payload["metadata"] = metadata
        return await self._request("POST", "/v1/interventions", json_data=payload)

    async def get_intervention(self, intervention_id: str) -> Dict[str, Any]:
        """Get an intervention by ID.

        Example:
            >>> intv = await nx.get_intervention("intv_abc123")
            >>> print(intv["intervention_type"], intv["actor"])
        """
        return await self._request("GET", f"/v1/interventions/{intervention_id}")

    async def list_interventions(
        self,
        *,
        mission_id: Optional[str] = None,
        agent_id: Optional[str] = None,
        intervention_type: Optional[str] = None,
        limit: int = 100,
        offset: int = 0,
    ) -> Dict[str, Any]:
        """List interventions, optionally filtered.

        Example:
            >>> result = await nx.list_interventions(mission_id="mis_abc")
            >>> for i in result["interventions"]:
            ...     print(i["intervention_type"], i["actor"])
        """
        params: Dict[str, Any] = {"limit": limit, "offset": offset}
        if mission_id:
            params["mission_id"] = mission_id
        if agent_id:
            params["agent_id"] = agent_id
        if intervention_type:
            params["type"] = intervention_type
        return await self._request("GET", "/v1/interventions", params=params)
