"""MemoryAI SDK — sync + async clients."""
from dataclasses import dataclass, field
from typing import Optional
import asyncio
import time
import httpx


# P2 #5: Retry config — exponential backoff for network errors and 5xx responses.
_MAX_RETRIES = 3
_BACKOFF_SECONDS = [1, 2, 4]
_RETRYABLE_STATUS = {502, 503, 504, 429}


def _is_retryable(exc: Exception) -> bool:
    """Check if an exception is retryable (network error or 5xx)."""
    if isinstance(exc, (httpx.ConnectError, httpx.ReadTimeout, httpx.WriteTimeout,
                        httpx.PoolTimeout, httpx.ConnectTimeout)):
        return True
    if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code in _RETRYABLE_STATUS:
        return True
    return False


@dataclass
class MemoryResult:
    id: int
    content: str
    score: float
    score_breakdown: dict
    source: Optional[str] = None
    tags: list[str] = field(default_factory=list)
    created_at: str = ""
    content_type: Optional[str] = None
    metadata: Optional[dict] = None
    memory_type: Optional[str] = None


@dataclass
class StoreResult:
    id: int
    layers: list[str] = field(default_factory=list)
    deduplicated: bool = False
    similar_chunk_id: Optional[int] = None
    similarity: Optional[float] = None
    memory_type: Optional[str] = None
    retention: Optional[str] = None
    contradiction_warning: Optional[str] = None


@dataclass
class CompactResult:
    chunks_created: int = 0
    chunks_deduplicated: int = 0
    index: list[dict] = field(default_factory=list)


@dataclass
class IndexProjectResult:
    files_indexed: int = 0
    chunks_stored: int = 0
    chunks_deduplicated: int = 0
    tree_size: int = 0


@dataclass
class Stats:
    tenant_id: str = ""
    plan: str = "free"
    chunks: int = 0
    storage_mb: float = 0.0
    usage_this_month: dict = field(default_factory=dict)


@dataclass
class CollectiveResult:
    id: int
    content: str
    score: float
    content_type: str = "bug_fix"
    stack_tags: list[str] = field(default_factory=list)
    language: str = "unknown"
    confidence_score: float = 0.0
    usage_count: int = 0
    contributor_count: int = 1
    first_seen: str = ""
    last_confirmed: str = ""
    source: str = "collective"


@dataclass
class ContributeResult:
    collective_chunk_id: int
    is_new: bool = True
    contributor_count: int = 1
    confidence_score: float = 0.0
    shard_domain: Optional[str] = None
    shard_subdomain: Optional[str] = None


@dataclass
class ConfirmResult:
    confidence_score: float = 0.0
    positive_count: int = 0
    total_count: int = 0


@dataclass
class CollectiveStats:
    total_chunks: int = 0
    total_contributions: int = 0
    total_confirmations: int = 0
    top_tags: dict = field(default_factory=dict)
    top_types: dict = field(default_factory=dict)


@dataclass
class L2StoreResult:
    id: int
    bank_name: str = ""
    token_count: int = 0
    expires_at: Optional[str] = None


@dataclass
class L2RecallResult:
    answer: str = ""
    tokens_used: int = 0
    banks_searched: list[str] = field(default_factory=list)


@dataclass
class L2CompressResult:
    compressed: bool = False
    reason: Optional[str] = None
    entries_compressed: int = 0
    original_tokens: int = 0
    digest_tokens: int = 0


@dataclass
class L2Stats:
    banks: list[dict] = field(default_factory=list)
    today_calls: int = 0
    today_tokens: int = 0
    plan_limit: dict = field(default_factory=dict)


@dataclass
class ShardInfo:
    id: int = 0
    domain: str = ""
    subdomain: str = ""
    chunk_count: int = 0
    created_at: str = ""


@dataclass
class SynthesisStats:
    cards_created: int = 0
    chunks_superseded: int = 0
    total_cards: int = 0
    by_shard: list = field(default_factory=list)


@dataclass
class HandoffResult:
    handoff_id: Optional[str] = None
    turns_stored: int = 0
    turns_count: int = 0
    tokens_snapshot: int = 0
    status: str = "not_found"
    conversation: list = field(default_factory=list)
    memories: list = field(default_factory=list)
    chunks_archived: int = 0
    active: bool = False
    created_at: str = ""
    completed_at: Optional[str] = None


class AsyncMemoryAI:
    """Async client for MemoryAI."""

    def __init__(
        self,
        api_key: str,
        base_url: str = "http://localhost:8420",
        timeout: float = 30.0,
    ):
        self._client = httpx.AsyncClient(
            base_url=base_url,
            headers={"Authorization": f"Bearer {api_key}"},
            timeout=timeout,
        )

    async def _request(self, method: str, url: str, **kwargs) -> httpx.Response:
        """Send request with retry + exponential backoff (P2 #5)."""
        last_exc: Exception | None = None
        for attempt in range(_MAX_RETRIES):
            try:
                resp = await self._client.request(method, url, **kwargs)
                resp.raise_for_status()
                return resp
            except Exception as exc:
                last_exc = exc
                if not _is_retryable(exc) or attempt == _MAX_RETRIES - 1:
                    raise
                await asyncio.sleep(_BACKOFF_SECONDS[attempt])
        raise last_exc  # unreachable, but keeps type-checker happy

    async def store(
        self,
        content: str,
        source: Optional[str] = None,
        tags: Optional[list[str]] = None,
        priority: str = "warm",
        language: str = "english",
        content_type: Optional[str] = None,
        metadata: Optional[dict] = None,
        zone: str = "standard",
        importance: float = 0.5,
    ) -> StoreResult:
        body: dict = {
            "content": content,
            "source": source,
            "tags": tags or [],
            "priority": priority,
            "language": language,
            "zone": zone,
            "importance": importance,
        }
        if content_type:
            body["content_type"] = content_type
        if metadata:
            body["metadata"] = metadata
        resp = await self._request("POST", "/v1/store", json=body)
        return StoreResult(**resp.json())

    async def store_code(
        self,
        content: str,
        file_path: str,
        language: Optional[str] = None,
        symbol: Optional[str] = None,
        symbol_type: str = "module",
        source: Optional[str] = None,
        tags: Optional[list[str]] = None,
    ) -> StoreResult:
        """Convenience: store code with IDE metadata."""
        meta = {"file": file_path, "symbol_type": symbol_type}
        if language:
            meta["language"] = language
        if symbol:
            meta["symbol"] = symbol
        return await self.store(
            content=content,
            source=source or f"ide:{file_path}",
            tags=(tags or []) + ["code"],
            content_type="code",
            metadata=meta,
        )

    async def recall(
        self,
        query: str,
        depth: str = "deep",
        limit: int = 5,
        min_score: float = 0.0,
        tags: Optional[list[str]] = None,
        max_tokens: Optional[int] = None,
        priority_min: Optional[str] = None,
    ) -> list[MemoryResult]:
        body: dict = {
            "query": query,
            "depth": depth,
            "limit": limit,
            "min_score": min_score,
            "tags": tags or [],
        }
        if max_tokens is not None:
            body["max_tokens"] = max_tokens
        if priority_min is not None:
            body["priority_min"] = priority_min
        resp = await self._request("POST", "/v1/recall", json=body)
        data = resp.json()
        return [MemoryResult(**r) for r in data["results"]]

    async def stats(self) -> Stats:
        resp = await self._request("GET", "/v1/stats")
        return Stats(**resp.json())

    async def compact(
        self,
        content: str,
        task_context: Optional[str] = None,
        content_type: Optional[str] = None,
        metadata: Optional[dict] = None,
    ) -> CompactResult:
        body: dict = {"content": content, "task_context": task_context}
        if content_type:
            body["content_type"] = content_type
        if metadata:
            body["metadata"] = metadata
        resp = await self._request("POST", "/v1/context/compact", json=body)
        return CompactResult(**resp.json())

    async def compact_project(
        self,
        content: str,
        file_path: str,
        language: Optional[str] = None,
        task_context: Optional[str] = None,
    ) -> CompactResult:
        """Convenience: compact code with IDE metadata."""
        meta = {"file_path": file_path}
        if language:
            meta["language"] = language
        return await self.compact(
            content=content,
            task_context=task_context,
            content_type="code",
            metadata=meta,
        )

    async def index_project(
        self,
        file_tree: list[str],
        key_files: Optional[dict[str, str]] = None,
        git_info: Optional[dict] = None,
    ) -> IndexProjectResult:
        """Index a project's file tree and key files."""
        body: dict = {"file_tree": file_tree}
        if key_files:
            body["key_files"] = key_files
        if git_info:
            body["git_info"] = git_info
        resp = await self._request("POST", "/v1/context/index-project", json=body)
        return IndexProjectResult(**resp.json())

    async def _contribute(
        self,
        content: str,
        content_type: str = "bug_fix",
        stack_tags: Optional[list[str]] = None,
        language: str = "unknown",
        metadata: Optional[dict] = None,
    ) -> ContributeResult:
        resp = await self._request("POST", "/v1/collective/contribute", json={
            "content": content,
            "content_type": content_type,
            "stack_tags": stack_tags or [],
            "language": language,
            "metadata": metadata,
        })
        return ContributeResult(**resp.json())

    async def _recall_collective(
        self,
        query: str,
        depth: str = "deep",
        limit: int = 10,
        stack_tags: Optional[list[str]] = None,
        content_type: Optional[str] = None,
        shard_filter: Optional[list[int]] = None,
    ) -> list[CollectiveResult]:
        body: dict = {"query": query, "depth": depth, "limit": limit, "stack_tags": stack_tags or []}
        if content_type:
            body["content_type"] = content_type
        if shard_filter:
            body["shard_filter"] = shard_filter
        resp = await self._request("POST", "/v1/collective/recall", json=body)
        return [CollectiveResult(**r) for r in resp.json()["results"]]

    async def _collective_shards(self) -> list[ShardInfo]:
        resp = await self._request("GET", "/v1/collective/shards")
        return [ShardInfo(**s) for s in resp.json()["shards"]]

    async def _collective_synthesize(self) -> SynthesisStats:
        resp = await self._request("POST", "/v1/collective/synthesize")
        return SynthesisStats(**resp.json())

    async def _confirm(self, collective_chunk_id: int, worked: bool = True) -> ConfirmResult:
        resp = await self._request("POST", "/v1/collective/confirm", json={
            "collective_chunk_id": collective_chunk_id, "worked": worked,
        })
        return ConfirmResult(**resp.json())

    async def _collective_stats(self) -> CollectiveStats:
        resp = await self._request("GET", "/v1/collective/stats")
        return CollectiveStats(**resp.json())

    async def _collective_opt_in(self) -> dict:
        resp = await self._request("POST", "/v1/collective/opt-in")
        return resp.json()

    async def _collective_opt_out(self) -> dict:
        resp = await self._request("POST", "/v1/collective/opt-out")
        return resp.json()

    async def export_data(self) -> list[dict]:
        resp = await self._request("GET", "/v1/export")
        return resp.json()

    async def l2_store(self, bank_name: str, content: str, token_count: int = 0) -> L2StoreResult:
        resp = await self._request("POST", "/v1/l2/store", json={
            "bank_name": bank_name, "content": content, "token_count": token_count,
        })
        return L2StoreResult(**resp.json())

    async def l2_recall(self, query: str, bank_names: Optional[list[str]] = None) -> L2RecallResult:
        body: dict = {"query": query}
        if bank_names:
            body["bank_names"] = bank_names
        resp = await self._request("POST", "/v1/l2/recall", json=body)
        return L2RecallResult(**resp.json())

    async def l2_compress(self, bank_name: str) -> L2CompressResult:
        resp = await self._request("POST", "/v1/l2/compress", json={"bank_name": bank_name})
        return L2CompressResult(**resp.json())

    async def l2_stats(self) -> L2Stats:
        resp = await self._request("GET", "/v1/l2/stats")
        return L2Stats(**resp.json())

    async def import_data(self, chunks: list[dict]) -> dict:
        resp = await self._request("POST", "/v1/import", json=chunks)
        return resp.json()

    async def delete_data(self) -> dict:
        resp = await self._request("DELETE", "/v1/data")
        return resp.json()

    async def _pool_stats(self) -> dict:
        resp = await self._request("GET", "/v1/pool/stats")
        return resp.json()

    async def list_entities(self, entity_type: Optional[str] = None, limit: int = 50) -> dict:
        """List tracked entities."""
        params: dict = {"limit": limit}
        if entity_type:
            params["entity_type"] = entity_type
        resp = await self._request("GET", "/v1/entities", params=params)
        return resp.json()

    async def entity_chunks(self, entity_name: str, limit: int = 20) -> dict:
        """Get chunk IDs linked to an entity."""
        resp = await self._request("GET", f"/v1/entities/{entity_name}/chunks", params={"limit": limit})
        return resp.json()

    async def learn(
        self,
        action: str,
        result: str,
        outcome: str = "success",
        lesson: Optional[str] = None,
        tags: Optional[list[str]] = None,
    ) -> dict:
        """Store an action + result + lesson learned."""
        body: dict = {"action": action, "result": result, "outcome": outcome}
        if lesson:
            body["lesson"] = lesson
        if tags:
            body["tags"] = tags
        resp = await self._request("POST", "/v1/learn", json=body)
        return resp.json()

    async def health_detailed(self) -> dict:
        """Get detailed memory health — chunk tiers, stale count, recommendations."""
        resp = await self._request("GET", "/v1/health/detailed")
        return resp.json()

    async def session_recover(
        self,
        task_context: Optional[str] = None,
        time_range_hours: int = 24,
        max_tokens: Optional[int] = 8000,
        session_id: Optional[str] = None,
    ) -> dict:
        """Recover session context from recent memory chunks."""
        body: dict = {"time_range_hours": time_range_hours}
        if task_context:
            body["task_context"] = task_context
        if max_tokens is not None:
            body["max_tokens"] = max_tokens
        if session_id:
            body["session_id"] = session_id
        resp = await self._request("POST", "/v1/session/recover", json=body)
        return resp.json()

    async def context_monitor(
        self,
        current_tokens: int,
        max_tokens: int = 200000,
        session_id: Optional[str] = None,
    ) -> dict:
        """Monitor context window usage and get compaction recommendations."""
        body: dict = {"current_tokens": current_tokens, "max_tokens": max_tokens}
        if session_id:
            body["session_id"] = session_id
        resp = await self._request("POST", "/v1/context/monitor", json=body)
        return resp.json()

    async def snapshot_create(self) -> dict:
        """Create a snapshot backup of all current memory chunks."""
        resp = await self._request("POST", "/v1/snapshots/create")
        return resp.json()

    async def snapshot_list(self) -> dict:
        """List all available memory snapshots."""
        resp = await self._request("GET", "/v1/snapshots")
        return resp.json()

    async def snapshot_restore(self, snapshot_id: str) -> dict:
        """Restore memory from a snapshot."""
        resp = await self._request("POST", f"/v1/snapshots/{snapshot_id}/restore")
        return resp.json()

    async def bootstrap(
        self,
        task_description: Optional[str] = None,
        project_name: Optional[str] = None,
        max_tokens: int = 4000,
    ) -> dict:
        """Get a ready-to-use context block for session start."""
        body: dict = {"max_tokens": max_tokens}
        if task_description:
            body["task_description"] = task_description
        if project_name:
            body["project_name"] = project_name
        resp = await self._request("POST", "/v1/memory/bootstrap", json=body)
        return resp.json()

    async def explore(self, chunk_id: int, limit: int = 10) -> dict:
        """Explore neural graph neighbors of a chunk."""
        resp = await self._request("GET", f"/v1/graph/neighbors/{chunk_id}", params={"limit": limit})
        return resp.json()

    async def clusters(self, limit: int = 20) -> dict:
        """Get memory topic clusters from the neural graph."""
        resp = await self._request("GET", "/v1/graph/clusters", params={"limit": limit})
        return resp.json()

    # ── Session Handoff ───────────────────────────────────

    async def handoff_start(
        self,
        conversation: list[dict],
        metadata: Optional[dict] = None,
    ) -> HandoffResult:
        """Start session handoff: store old session conversation on server."""
        body: dict = {"conversation": conversation}
        if metadata:
            body["metadata"] = metadata
        resp = await self._request("POST", "/v1/session/handoff/start", json=body)
        return HandoffResult(**resp.json())

    async def handoff_restore(
        self,
        handoff_id: Optional[str] = None,
        include_memories: bool = True,
        memory_limit: int = 5,
    ) -> HandoffResult:
        """Restore old session conversation + MemoryAI memories for new session."""
        body: dict = {"include_memories": include_memories, "memory_limit": memory_limit}
        if handoff_id:
            body["handoff_id"] = handoff_id
        resp = await self._request("POST", "/v1/session/handoff/restore", json=body)
        return HandoffResult(**resp.json())

    async def handoff_complete(
        self,
        handoff_id: Optional[str] = None,
        archive_to_memory: bool = True,
    ) -> HandoffResult:
        """Complete handoff: archive old session into long-term MemoryAI storage."""
        body: dict = {"archive_to_memory": archive_to_memory}
        if handoff_id:
            body["handoff_id"] = handoff_id
        resp = await self._request("POST", "/v1/session/handoff/complete", json=body)
        return HandoffResult(**resp.json())

    async def handoff_status(self) -> HandoffResult:
        """Check current handoff status."""
        resp = await self._request("GET", "/v1/session/handoff/status")
        return HandoffResult(**resp.json())

    async def close(self):
        await self._client.aclose()

    async def __aenter__(self):
        return self

    async def __aexit__(self, *args):
        await self.close()


class MemoryAI:
    """Sync client for MemoryAI."""

    def __init__(
        self,
        api_key: str,
        base_url: str = "http://localhost:8420",
        timeout: float = 30.0,
    ):
        self._client = httpx.Client(
            base_url=base_url,
            headers={"Authorization": f"Bearer {api_key}"},
            timeout=timeout,
        )

    def _request(self, method: str, url: str, **kwargs) -> httpx.Response:
        """Send request with retry + exponential backoff (P2 #5)."""
        last_exc: Exception | None = None
        for attempt in range(_MAX_RETRIES):
            try:
                resp = self._client.request(method, url, **kwargs)
                resp.raise_for_status()
                return resp
            except Exception as exc:
                last_exc = exc
                if not _is_retryable(exc) or attempt == _MAX_RETRIES - 1:
                    raise
                time.sleep(_BACKOFF_SECONDS[attempt])
        raise last_exc  # unreachable, but keeps type-checker happy

    def store(
        self,
        content: str,
        source: Optional[str] = None,
        tags: Optional[list[str]] = None,
        priority: str = "warm",
        language: str = "english",
        content_type: Optional[str] = None,
        metadata: Optional[dict] = None,
        zone: str = "standard",
        importance: float = 0.5,
    ) -> StoreResult:
        body: dict = {
            "content": content,
            "source": source,
            "tags": tags or [],
            "priority": priority,
            "language": language,
            "zone": zone,
            "importance": importance,
        }
        if content_type:
            body["content_type"] = content_type
        if metadata:
            body["metadata"] = metadata
        resp = self._request("POST", "/v1/store", json=body)
        return StoreResult(**resp.json())

    def store_code(
        self,
        content: str,
        file_path: str,
        language: Optional[str] = None,
        symbol: Optional[str] = None,
        symbol_type: str = "module",
        source: Optional[str] = None,
        tags: Optional[list[str]] = None,
    ) -> StoreResult:
        """Convenience: store code with IDE metadata."""
        meta = {"file": file_path, "symbol_type": symbol_type}
        if language:
            meta["language"] = language
        if symbol:
            meta["symbol"] = symbol
        return self.store(
            content=content,
            source=source or f"ide:{file_path}",
            tags=(tags or []) + ["code"],
            content_type="code",
            metadata=meta,
        )

    def recall(
        self,
        query: str,
        depth: str = "deep",
        limit: int = 5,
        min_score: float = 0.0,
        tags: Optional[list[str]] = None,
        max_tokens: Optional[int] = None,
        priority_min: Optional[str] = None,
    ) -> list[MemoryResult]:
        body: dict = {
            "query": query,
            "depth": depth,
            "limit": limit,
            "min_score": min_score,
            "tags": tags or [],
        }
        if max_tokens is not None:
            body["max_tokens"] = max_tokens
        if priority_min is not None:
            body["priority_min"] = priority_min
        resp = self._request("POST", "/v1/recall", json=body)
        data = resp.json()
        return [MemoryResult(**r) for r in data["results"]]

    def stats(self) -> Stats:
        resp = self._request("GET", "/v1/stats")
        return Stats(**resp.json())

    def compact(
        self,
        content: str,
        task_context: Optional[str] = None,
        content_type: Optional[str] = None,
        metadata: Optional[dict] = None,
    ) -> CompactResult:
        body: dict = {"content": content, "task_context": task_context}
        if content_type:
            body["content_type"] = content_type
        if metadata:
            body["metadata"] = metadata
        resp = self._request("POST", "/v1/context/compact", json=body)
        return CompactResult(**resp.json())

    def compact_project(
        self,
        content: str,
        file_path: str,
        language: Optional[str] = None,
        task_context: Optional[str] = None,
    ) -> CompactResult:
        """Convenience: compact code with IDE metadata."""
        meta = {"file_path": file_path}
        if language:
            meta["language"] = language
        return self.compact(
            content=content,
            task_context=task_context,
            content_type="code",
            metadata=meta,
        )

    def index_project(
        self,
        file_tree: list[str],
        key_files: Optional[dict[str, str]] = None,
        git_info: Optional[dict] = None,
    ) -> IndexProjectResult:
        """Index a project's file tree and key files."""
        body: dict = {"file_tree": file_tree}
        if key_files:
            body["key_files"] = key_files
        if git_info:
            body["git_info"] = git_info
        resp = self._request("POST", "/v1/context/index-project", json=body)
        return IndexProjectResult(**resp.json())

    def _contribute(
        self,
        content: str,
        content_type: str = "bug_fix",
        stack_tags: Optional[list[str]] = None,
        language: str = "unknown",
        metadata: Optional[dict] = None,
    ) -> ContributeResult:
        resp = self._request("POST", "/v1/collective/contribute", json={
            "content": content,
            "content_type": content_type,
            "stack_tags": stack_tags or [],
            "language": language,
            "metadata": metadata,
        })
        return ContributeResult(**resp.json())

    def _recall_collective(
        self,
        query: str,
        depth: str = "deep",
        limit: int = 10,
        stack_tags: Optional[list[str]] = None,
        content_type: Optional[str] = None,
        shard_filter: Optional[list[int]] = None,
    ) -> list[CollectiveResult]:
        body: dict = {"query": query, "depth": depth, "limit": limit, "stack_tags": stack_tags or []}
        if content_type:
            body["content_type"] = content_type
        if shard_filter:
            body["shard_filter"] = shard_filter
        resp = self._request("POST", "/v1/collective/recall", json=body)
        return [CollectiveResult(**r) for r in resp.json()["results"]]

    def _collective_shards(self) -> list[ShardInfo]:
        resp = self._request("GET", "/v1/collective/shards")
        return [ShardInfo(**s) for s in resp.json()["shards"]]

    def _collective_synthesize(self) -> SynthesisStats:
        resp = self._request("POST", "/v1/collective/synthesize")
        return SynthesisStats(**resp.json())

    def _confirm(self, collective_chunk_id: int, worked: bool = True) -> ConfirmResult:
        resp = self._request("POST", "/v1/collective/confirm", json={
            "collective_chunk_id": collective_chunk_id, "worked": worked,
        })
        return ConfirmResult(**resp.json())

    def _collective_stats(self) -> CollectiveStats:
        resp = self._request("GET", "/v1/collective/stats")
        return CollectiveStats(**resp.json())

    def _collective_opt_in(self) -> dict:
        resp = self._request("POST", "/v1/collective/opt-in")
        return resp.json()

    def _collective_opt_out(self) -> dict:
        resp = self._request("POST", "/v1/collective/opt-out")
        return resp.json()

    def export_data(self) -> list[dict]:
        resp = self._request("GET", "/v1/export")
        return resp.json()

    def l2_store(self, bank_name: str, content: str, token_count: int = 0) -> L2StoreResult:
        resp = self._request("POST", "/v1/l2/store", json={
            "bank_name": bank_name, "content": content, "token_count": token_count,
        })
        return L2StoreResult(**resp.json())

    def l2_recall(self, query: str, bank_names: Optional[list[str]] = None) -> L2RecallResult:
        body: dict = {"query": query}
        if bank_names:
            body["bank_names"] = bank_names
        resp = self._request("POST", "/v1/l2/recall", json=body)
        return L2RecallResult(**resp.json())

    def l2_compress(self, bank_name: str) -> L2CompressResult:
        resp = self._request("POST", "/v1/l2/compress", json={"bank_name": bank_name})
        return L2CompressResult(**resp.json())

    def l2_stats(self) -> L2Stats:
        resp = self._request("GET", "/v1/l2/stats")
        return L2Stats(**resp.json())

    def import_data(self, chunks: list[dict]) -> dict:
        resp = self._request("POST", "/v1/import", json=chunks)
        return resp.json()

    def delete_data(self) -> dict:
        resp = self._request("DELETE", "/v1/data")
        return resp.json()

    def _pool_stats(self) -> dict:
        resp = self._request("GET", "/v1/pool/stats")
        return resp.json()

    def list_entities(self, entity_type: Optional[str] = None, limit: int = 50) -> dict:
        """List tracked entities."""
        params: dict = {"limit": limit}
        if entity_type:
            params["entity_type"] = entity_type
        resp = self._request("GET", "/v1/entities", params=params)
        return resp.json()

    def entity_chunks(self, entity_name: str, limit: int = 20) -> dict:
        """Get chunk IDs linked to an entity."""
        resp = self._request("GET", f"/v1/entities/{entity_name}/chunks", params={"limit": limit})
        return resp.json()

    def learn(
        self,
        action: str,
        result: str,
        outcome: str = "success",
        lesson: Optional[str] = None,
        tags: Optional[list[str]] = None,
    ) -> dict:
        """Store an action + result + lesson learned."""
        body: dict = {"action": action, "result": result, "outcome": outcome}
        if lesson:
            body["lesson"] = lesson
        if tags:
            body["tags"] = tags
        resp = self._request("POST", "/v1/learn", json=body)
        return resp.json()

    def health_detailed(self) -> dict:
        """Get detailed memory health — chunk tiers, stale count, recommendations."""
        resp = self._request("GET", "/v1/health/detailed")
        return resp.json()

    def session_recover(
        self,
        task_context: Optional[str] = None,
        time_range_hours: int = 24,
        max_tokens: Optional[int] = 8000,
        session_id: Optional[str] = None,
    ) -> dict:
        """Recover session context from recent memory chunks."""
        body: dict = {"time_range_hours": time_range_hours}
        if task_context:
            body["task_context"] = task_context
        if max_tokens is not None:
            body["max_tokens"] = max_tokens
        if session_id:
            body["session_id"] = session_id
        resp = self._request("POST", "/v1/session/recover", json=body)
        return resp.json()

    def context_monitor(
        self,
        current_tokens: int,
        max_tokens: int = 200000,
        session_id: Optional[str] = None,
    ) -> dict:
        """Monitor context window usage and get compaction recommendations."""
        body: dict = {"current_tokens": current_tokens, "max_tokens": max_tokens}
        if session_id:
            body["session_id"] = session_id
        resp = self._request("POST", "/v1/context/monitor", json=body)
        return resp.json()

    def snapshot_create(self) -> dict:
        """Create a snapshot backup of all current memory chunks."""
        resp = self._request("POST", "/v1/snapshots/create")
        return resp.json()

    def snapshot_list(self) -> dict:
        """List all available memory snapshots."""
        resp = self._request("GET", "/v1/snapshots")
        return resp.json()

    def snapshot_restore(self, snapshot_id: str) -> dict:
        """Restore memory from a snapshot."""
        resp = self._request("POST", f"/v1/snapshots/{snapshot_id}/restore")
        return resp.json()

    def bootstrap(
        self,
        task_description: Optional[str] = None,
        project_name: Optional[str] = None,
        max_tokens: int = 4000,
    ) -> dict:
        """Get a ready-to-use context block for session start."""
        body: dict = {"max_tokens": max_tokens}
        if task_description:
            body["task_description"] = task_description
        if project_name:
            body["project_name"] = project_name
        resp = self._request("POST", "/v1/memory/bootstrap", json=body)
        return resp.json()

    def explore(self, chunk_id: int, limit: int = 10) -> dict:
        """Explore neural graph neighbors of a chunk."""
        resp = self._request("GET", f"/v1/graph/neighbors/{chunk_id}", params={"limit": limit})
        return resp.json()

    def clusters(self, limit: int = 20) -> dict:
        """Get memory topic clusters from the neural graph."""
        resp = self._request("GET", "/v1/graph/clusters", params={"limit": limit})
        return resp.json()

    # ── Session Handoff ───────────────────────────────────

    def handoff_start(
        self,
        conversation: list[dict],
        metadata: Optional[dict] = None,
    ) -> HandoffResult:
        """Start session handoff: store old session conversation on server."""
        body: dict = {"conversation": conversation}
        if metadata:
            body["metadata"] = metadata
        resp = self._request("POST", "/v1/session/handoff/start", json=body)
        return HandoffResult(**resp.json())

    def handoff_restore(
        self,
        handoff_id: Optional[str] = None,
        include_memories: bool = True,
        memory_limit: int = 5,
    ) -> HandoffResult:
        """Restore old session conversation + MemoryAI memories for new session."""
        body: dict = {"include_memories": include_memories, "memory_limit": memory_limit}
        if handoff_id:
            body["handoff_id"] = handoff_id
        resp = self._request("POST", "/v1/session/handoff/restore", json=body)
        return HandoffResult(**resp.json())

    def handoff_complete(
        self,
        handoff_id: Optional[str] = None,
        archive_to_memory: bool = True,
    ) -> HandoffResult:
        """Complete handoff: archive old session into long-term MemoryAI storage."""
        body: dict = {"archive_to_memory": archive_to_memory}
        if handoff_id:
            body["handoff_id"] = handoff_id
        resp = self._request("POST", "/v1/session/handoff/complete", json=body)
        return HandoffResult(**resp.json())

    def handoff_status(self) -> HandoffResult:
        """Check current handoff status."""
        resp = self._request("GET", "/v1/session/handoff/status")
        return HandoffResult(**resp.json())

    def close(self):
        self._client.close()

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.close()


# Backward compatibility aliases
HybridMemory = MemoryAI
AsyncHybridMemory = AsyncMemoryAI
