"""Main SDK class - the developer-facing interface."""

import asyncio
import json
import logging
from datetime import datetime, timezone
from typing import Any, Callable, Dict, List, Optional

from .config_utils import configure_logging, get_default_storage_path, merge_config
from .registry import SDKRegistry
from .models.config import SDKConfig, TimeoutConfig, RetryPolicy
from .models.context import (
    ContextResponse,
    CompactionResponse,
    CompactionStatusResponse,
    CompactionTriggerResponse,
    ContextForPromptResponse,
    ResponseMetadata,
)
from .models.enums import CompactionLevel, ContextScope
from .models.errors import (
    SynapError,
    BootstrapError,
    AuthenticationError,
    InvalidInputError,
    ContextNotFoundError,
)
from .auth.manager import CredentialManager
from .cache.manager import CacheManager, CacheScope
from .transport.http_client import HTTPTransport
from .transport.grpc_client import GRPCTransport
from .telemetry.collector import TelemetryCollector, emit_fetch_event
from .telemetry.transport import TelemetryTransport
from .utils.correlation import generate_correlation_id
from ._version import __version__
from .memories.interface import MemoriesInterface
from .facade.instance import InstanceController
from .facade.conversation import ConversationController


logger = logging.getLogger("synap.sdk")


def _build_anticipation_response(
    bundle: Dict,
    correlation_id: str,
    start_time: datetime,
    scope: str,
    mode: str,
    telemetry_collector,
) -> ContextResponse:
    """Convert an anticipated bundle to a ContextResponse.

    Shared helper used by all scope fetch methods when the anticipation
    cache returns a hit.
    """
    items_by_type = bundle.get("items_by_type", {})
    context_data = {ctx_type: items for ctx_type, items in items_by_type.items()}
    if bundle.get("conversation_context"):
        context_data["conversation_context"] = bundle["conversation_context"]
    metadata = ResponseMetadata(
        correlation_id=correlation_id,
        ttl_seconds=0,
        source="anticipation",
        retrieved_at=datetime.now(timezone.utc),
    )
    latency_ms = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000)
    emit_fetch_event(
        telemetry_collector,
        scope=scope,
        correlation_id=correlation_id,
        latency_ms=latency_ms,
        cache_hit=True,
        mode=mode,
        cache_origin="anticipation_cache",
    )
    return ContextResponse.from_cloud_response(context_data, metadata)


async def _emit_context_fetch_event(
    sdk: "MaximemSynapSDK",
    scope: str,
    search_query: Optional[List[str]],
    types: Optional[List[str]],
    mode: str,
    source: str,
    items_count: int,
    conversation_id: str = "",
    user_id: str = "",
    customer_id: str = "",
) -> None:
    """Auto-emit a context_fetch event over gRPC stream (fire-and-forget).

    This is the bridge between the explicit path (fetch) and the implicit
    path (anticipation). The server accumulates these in retrieval_history
    to inform the anticipation LLM.
    """
    try:
        if not sdk.instance.is_listening:
            return
        await sdk.instance._controller._transport.send({
            "event_type": "context_fetch",
            "content": "",
            "role": "system",
            "conversation_id": conversation_id,
            "user_id": user_id,
            "customer_id": customer_id,
            "session_id": "",
            "search_queries": search_query or [],
            "context_types": types or [],
            "metadata": {
                "source": source,
                "items_count": str(items_count),
                "mode": mode,
                "scope": scope,
            },
        })
    except Exception as e:
        logger.debug("context_fetch emit failed (non-fatal): %s", e)


def _merge_user_summary_into_response(
    response: ContextResponse,
    summary_bundle: Dict,
    max_summary_items: int = 3,
) -> ContextResponse:
    """Mix a few items from the user summary into an existing response.

    Adds up to max_summary_items per context type from the summary,
    skipping items already present in the response (by id).
    """
    _TYPE_TO_ATTR = {
        "facts": "facts",
        "preferences": "preferences",
        "episodes": "episodes",
        "emotions": "emotions",
        "temporal_events": "temporal_events",
    }
    summary_items = summary_bundle.get("items_by_type", {})
    for ctx_type, items in summary_items.items():
        attr_name = _TYPE_TO_ATTR.get(ctx_type)
        if attr_name is None or not isinstance(items, list):
            continue
        existing = getattr(response, attr_name)
        existing_ids = {item.id for item in existing}
        new_raw = [
            item for item in items
            if isinstance(item, dict) and item.get("item_id") not in existing_ids
        ][:max_summary_items]
        if not new_raw:
            continue
        # Normalize gRPC bundle keys to match from_cloud_response expectations:
        # bundles use "item_id"/"context_type"/"created_at"; the factory reads
        # "id"/"category"/"extracted_at" (and "emotion_type" for emotions).
        normalized = [
            {
                "id": item.get("item_id", item.get("id", "")),
                "content": item.get("content", ""),
                "confidence": item.get("confidence", 0.0),
                "source": item.get("source", ""),
                "category": item.get("context_type", item.get("category", "")),
                "emotion_type": item.get("context_type", item.get("emotion_type", "")),
                "strength": item.get("strength", item.get("confidence", 0.0)),
                "summary": item.get("summary", item.get("content", "")),
                "significance": item.get("significance", item.get("confidence", 0.0)),
                "intensity": item.get("intensity", item.get("confidence", 0.0)),
                "context": item.get("context", item.get("content", "")),
                "participants": item.get("participants", []),
                "extracted_at": item.get("extracted_at") or item.get("created_at"),
                "occurred_at": item.get("occurred_at") or item.get("extracted_at") or item.get("created_at"),
                "detected_at": item.get("detected_at") or item.get("extracted_at") or item.get("created_at"),
                "metadata": item.get("metadata", {}),
                "event_date": item.get("event_date"),
                "valid_until": item.get("valid_until"),
                "temporal_category": item.get("temporal_category"),
                "temporal_confidence": item.get("temporal_confidence", 0.0),
            }
            for item in new_raw
        ]
        typed_chunk = ContextResponse.from_cloud_response(
            {ctx_type: normalized}, response.metadata
        )
        existing.extend(getattr(typed_chunk, attr_name))
    return response


def _count_context_items(response: ContextResponse) -> int:
    return (
        len(response.facts)
        + len(response.preferences)
        + len(response.episodes)
        + len(response.emotions)
        + len(response.temporal_events)
    )


class MaximemSynapSDK:
    """Synap SDK - Agentic Context Management.

    Usage:
        sdk = MaximemSynapSDK(instance_id="your-instance-id")
        await sdk.initialize(bootstrap_token="...")

        # Fetch context
        ctx = await sdk.conversation.context.fetch(conversation_id="...")

        # Compact context
        compacted = await sdk.conversation.context.compact(conversation_id="...")

        # Listen to agent activity
        await sdk.instance.listen()
    """

    def __init__(
        self,
        instance_id: str,
        bootstrap_token: Optional[str] = None,
        config: Optional[SDKConfig] = None,
        _force_new: bool = False,
    ):
        """Create or get SDK instance.

        Args:
            instance_id: Your Synap instance ID from the dashboard
            bootstrap_token: One-time token for initial setup (required on first use)
            config: Optional configuration overrides
            _force_new: Force create new instance (for testing)
        """
        # Check singleton registry
        if not _force_new:
            existing = SDKRegistry.get(instance_id)
            if existing is not None:
                # Return existing instance - copy its state
                self.__dict__ = existing.__dict__
                return

        self.instance_id = instance_id
        self._bootstrap_token = bootstrap_token
        self._config = config or SDKConfig()
        self._initialized = False

        # Components (initialized lazily or in initialize())
        self._credential_manager: Optional[CredentialManager] = None
        self._cache_manager: Optional[CacheManager] = None
        self._http_transport: Optional[HTTPTransport] = None
        self._grpc_transport: Optional[GRPCTransport] = None
        self._telemetry_collector: Optional[TelemetryCollector] = None
        self._telemetry_transport: Optional[TelemetryTransport] = None
        self._client_id: Optional[str] = None

        # Anticipation cache: stores bundles pushed proactively via gRPC stream
        from .cache.anticipation_cache import AnticipationCache
        self._anticipation_cache = AnticipationCache()

        # Per-conversation turn counter for periodic user summary injection
        self._turn_counters: Dict[str, int] = {}
        self._user_summary_interval: int = 5  # inject every N turns

        # Sub-interfaces
        self.conversation = ConversationInterface(self)
        self.user = UserInterface(self)
        self.customer = CustomerInterface(self)
        self.client = ClientInterface(self)
        self.instance = InstanceInterface(self)
        self.cache = CacheInterface(self)
        self.memories = MemoriesInterface(self)

        # Configure logging
        configure_logging(self._config.log_level)

        # Register in singleton registry
        if not _force_new:
            SDKRegistry.register(instance_id, self)

    def configure(self, **kwargs) -> None:
        """Update SDK configuration.

        Args:
            storage_path: Override default storage path
            credentials_source: "file" or "env"
            cache_backend: "sqlite" or None
            session_timeout_minutes: Session timeout (5-1440)
            timeouts: TimeoutConfig or dict
            retry_policy: RetryPolicy, dict, or None to disable
            log_level: "DEBUG", "INFO", "WARNING", "ERROR"
            logger: Custom logger instance
        """
        if self._initialized:
            raise InvalidInputError("Cannot reconfigure after initialization")

        # Handle custom logger
        if "logger" in kwargs:
            custom_logger = kwargs.pop("logger")
            # Replace the SDK logger
            global logger
            logger = custom_logger

        self._config = merge_config(self._config, kwargs)
        configure_logging(self._config.log_level)

    def _increment_turn(self, conversation_id: Optional[str]) -> int:
        """Increment and return the turn count for a conversation."""
        key = conversation_id or "_global"
        self._turn_counters[key] = self._turn_counters.get(key, 0) + 1
        return self._turn_counters[key]

    def _should_inject_user_summary(self, conversation_id: Optional[str]) -> bool:
        """Check if user summary should be injected this turn."""
        key = conversation_id or "_global"
        count = self._turn_counters.get(key, 0)
        return count > 0 and count % self._user_summary_interval == 0

    async def initialize(self, bootstrap_token: Optional[str] = None) -> None:
        """Initialize the SDK.

        Must be called before using any context operations.
        On first use, requires bootstrap_token from dashboard.

        Args:
            bootstrap_token: One-time token (required on first use)
        """
        if self._initialized:
            return

        token = bootstrap_token or self._bootstrap_token

        # Initialize credential manager
        self._credential_manager = CredentialManager(
            instance_id=self.instance_id,
            credentials_source=self._config.credentials_source,
            storage_path=self._config.storage_path,
        )

        # Create HTTP transport for auth
        auth_http = HTTPTransport(
            instance_id=self.instance_id,
            base_url=self._config.api_base_url,
            timeouts=self._config.timeouts,
            retry_policy=self._config.retry_policy,
        )
        self._credential_manager.set_auth_client(auth_http)

        # Bootstrap or load credentials
        try:
            credentials = await self._credential_manager.load_or_bootstrap(token)
            self._client_id = credentials.client_id
        except Exception as e:
            raise BootstrapError(f"SDK initialization failed: {e}") from e

        # Initialize cache
        if self._config.cache_backend:
            self._cache_manager = CacheManager(
                client_id=self._client_id,
                storage_path=self._config.storage_path,
                enabled=True,
            )
        else:
            self._cache_manager = CacheManager(
                client_id=self._client_id,
                enabled=False,
            )

        # Initialize main HTTP transport
        self._http_transport = HTTPTransport(
            instance_id=self.instance_id,
            base_url=self._config.api_base_url,
            timeouts=self._config.timeouts,
            retry_policy=self._config.retry_policy,
            telemetry_callback=self._on_telemetry_event,
        )

        # Initialize telemetry transport
        self._telemetry_transport = TelemetryTransport(
            base_url=self._http_transport.base_url,
            get_auth_context=self._get_auth_context_for_telemetry,
        )

        # Initialize telemetry collector
        self._telemetry_collector = TelemetryCollector(
            instance_id=self.instance_id,
            client_id=self._client_id,
            sdk_version=__version__,
            transport_callback=self._telemetry_transport.send,
            enabled=True,
        )
        await self._telemetry_collector.start()

        # Emit init event
        from .telemetry.models import TelemetryEventType
        self._telemetry_collector.emit(
            event_type=TelemetryEventType.SDK_INIT,
            status="success",
        )

        self._initialized = True
        logger.info(f"SDK initialized for instance {self.instance_id}")

    async def shutdown(self) -> None:
        """Gracefully shutdown the SDK.

        Flushes telemetry, closes connections, and releases resources.
        """
        logger.info("Shutting down SDK")

        # Emit shutdown event
        if self._telemetry_collector:
            from .telemetry.models import TelemetryEventType
            self._telemetry_collector.emit(
                event_type=TelemetryEventType.SDK_SHUTDOWN,
                status="success",
            )
            await self._telemetry_collector.stop()

        # Close telemetry transport
        if self._telemetry_transport:
            await self._telemetry_transport.close()

        # Close transports
        if self._http_transport:
            await self._http_transport.close()

        if self._grpc_transport:
            await self._grpc_transport.close()

        # Close cache
        if self._cache_manager:
            self._cache_manager.close()

        # Unregister from singleton
        SDKRegistry.unregister(self.instance_id)

        self._initialized = False
        logger.info("SDK shutdown complete")

    def _ensure_initialized(self) -> None:
        """Ensure SDK is initialized before operations."""
        if not self._initialized:
            raise BootstrapError(
                "SDK not initialized. Call await sdk.initialize() first."
            )

    async def _get_auth_context(self, correlation_id: Optional[str] = None):
        """Get auth context for requests."""
        self._ensure_initialized()
        return await self._credential_manager.get_auth_context(correlation_id)

    async def _get_auth_context_for_telemetry(self):
        """Get auth context for telemetry transport."""
        return await self._credential_manager.get_auth_context()

    def _on_telemetry_event(self, event: Dict[str, Any]) -> None:
        """Callback for transport telemetry events."""
        if self._telemetry_collector:
            self._telemetry_collector.emit_dict(event)


# Sub-interfaces for domain-oriented API

class ConversationInterface:
    """Interface for conversation-scoped operations."""

    def __init__(self, sdk: MaximemSynapSDK):
        self._sdk = sdk
        self.context = ConversationContextInterface(sdk)
        self._controller: Optional[ConversationController] = None

    def _ensure_controller(self) -> ConversationController:
        """Lazily create ConversationController after SDK is initialized."""
        if self._controller is None:
            self._sdk._ensure_initialized()
            self._controller = ConversationController(
                transport=self._sdk._http_transport,
                auth_provider=self._sdk._get_auth_context,
            )
        return self._controller

    async def record_message(
        self,
        conversation_id: str,
        role: str,
        content: str,
        user_id: str,
        customer_id: str,
        session_id: Optional[str] = None,
        metadata: Optional[Dict[str, Any]] = None,
    ) -> Dict[str, Any]:
        """Record a single conversation message.

        Args:
            conversation_id: Unique conversation identifier
            role: Message role ("user" or "assistant")
            content: Message content text
            user_id: User identifier (required, must be external ID)
            customer_id: Customer identifier (required, must be external ID)
            session_id: Session identifier (optional, auto-generated if not provided)
            metadata: Additional metadata (optional)

        Returns:
            Dict with message_id, conversation_id, session_id, recorded_at
        """
        controller = self._ensure_controller()
        correlation_id = generate_correlation_id(self._sdk.instance_id)
        return await controller.record_message(
            conversation_id=conversation_id,
            role=role,
            content=content,
            user_id=user_id,
            customer_id=customer_id,
            session_id=session_id,
            metadata=metadata,
            correlation_id=correlation_id,
        )

    async def record_messages_batch(
        self,
        messages: List[Dict[str, Any]],
    ) -> Dict[str, Any]:
        """Record multiple conversation messages in a batch.

        Args:
            messages: List of message dicts with conversation_id, role, content, etc.

        Returns:
            Dict with total, succeeded, failed, results[]
        """
        controller = self._ensure_controller()
        correlation_id = generate_correlation_id(self._sdk.instance_id)
        return await controller.record_messages_batch(
            messages=messages,
            correlation_id=correlation_id,
        )


class ConversationContextInterface:
    """Conversation context operations."""

    def __init__(self, sdk: MaximemSynapSDK):
        self._sdk = sdk

    async def fetch(
        self,
        conversation_id: str,
        search_query: Optional[List[str]] = None,
        max_results: int = 10,
        types: Optional[List[str]] = None,
        mode: str = "fast",
    ) -> ContextResponse:
        """Fetch context for a conversation.

        Args:
            conversation_id: The conversation to fetch context for
            search_query: Optional search queries
            max_results: Maximum results to return (default 10)
            types: Context types to include (default all)
            mode: Retrieval mode - "fast" (default) or "accurate"
                  - "fast": Direct query, low latency (~50-100ms)
                  - "accurate": LLM-enhanced queries, higher quality (~200-500ms)

        Returns:
            ContextResponse with facts, preferences, episodes, etc.
        """
        self._sdk._ensure_initialized()

        # Validate mode
        valid_modes = ("fast", "accurate")
        if mode not in valid_modes:
            raise InvalidInputError(f"Invalid mode '{mode}'. Must be one of: {valid_modes}")

        correlation_id = generate_correlation_id(self._sdk.instance_id)
        start_time = datetime.now(timezone.utc)
        cache_hit = False

        # Build cache key params
        cache_params = {
            "search_query": search_query,
            "max_results": max_results,
            "types": types,
            "mode": mode,
        }

        # Check anticipation cache first (bundles pre-fetched via gRPC stream)
        anticipated = self._sdk._anticipation_cache.lookup(
            search_query=search_query,
            conversation_id=conversation_id,
        )
        if anticipated:
            response = _build_anticipation_response(
                anticipated, correlation_id, start_time,
                scope="conversation", mode=mode,
                telemetry_collector=self._sdk._telemetry_collector,
            )
        else:
            # Check local cache
            cached = self._sdk._cache_manager.get(
                scope=CacheScope.CONVERSATION,
                entity_id=conversation_id,
                context_type="context",
                query=cache_params,
            )

            if cached:
                cache_hit = True
                data = json.loads(cached)
                metadata = ResponseMetadata(
                    correlation_id=correlation_id,
                    ttl_seconds=0,  # Already in cache
                    source="cache",
                    retrieved_at=datetime.now(timezone.utc),
                )
                response = ContextResponse.from_cloud_response(data, metadata)
            else:
                # Fetch from cloud
                auth_context = await self._sdk._get_auth_context(correlation_id)

                result = await self._sdk._http_transport.post(
                    "/v1/context/conversation/fetch",
                    auth_context=auth_context,
                    json={
                        "conversation_id": conversation_id,
                        "search_query": search_query,
                        "max_results": max_results,
                        "types": types or ["all"],
                        "mode": mode,
                    },
                    correlation_id=correlation_id,
                )

                metadata = ResponseMetadata(
                    correlation_id=correlation_id,
                    ttl_seconds=result.get("ttl_seconds", 300),
                    source="cloud",
                    retrieved_at=datetime.now(timezone.utc),
                )
                response = ContextResponse.from_cloud_response(result.get("context", {}), metadata)

                # Cache the result (skip empty responses to avoid caching transient failures)
                context_data = result.get("context", {})
                if any(context_data.get(k) for k in ("facts", "preferences", "episodes", "emotions", "temporal_events")):
                    self._sdk._cache_manager.set(
                        scope=CacheScope.CONVERSATION,
                        entity_id=conversation_id,
                        context_type="context",
                        value=json.dumps(context_data).encode(),
                        ttl_seconds=metadata.ttl_seconds,
                        query=cache_params,
                    )

            # Emit telemetry
            latency_ms = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000)
            cache_origin = "local_http_cache" if cache_hit else "cloud_fetch"
            emit_fetch_event(
                self._sdk._telemetry_collector,
                scope="conversation",
                correlation_id=correlation_id,
                latency_ms=latency_ms,
                cache_hit=cache_hit,
                mode=mode,
                cache_origin=cache_origin,
            )

        # Auto-emit context_fetch for server-side retrieval history
        items_count = len(response.facts) + len(response.preferences) + len(response.episodes) + len(response.emotions)
        asyncio.ensure_future(_emit_context_fetch_event(
            sdk=self._sdk,
            scope="conversation",
            search_query=search_query,
            types=types,
            mode=mode,
            source=response.metadata.source,
            items_count=items_count,
            conversation_id=conversation_id,
        ))

        # Periodic user summary injection
        turn = self._sdk._increment_turn(conversation_id)
        if self._sdk._should_inject_user_summary(conversation_id):
            summary = self._sdk._anticipation_cache.lookup_user_summary()
            if summary:
                response = _merge_user_summary_into_response(response, summary)
                logger.debug("Injected user summary at turn %d for conversation %s", turn, conversation_id)

        return response

    async def compact(
        self,
        conversation_id: str,
        strategy: Optional[str] = None,
        compaction_level: Optional[str] = None,
        target_tokens: Optional[int] = None,
        force: bool = False,
    ) -> CompactionTriggerResponse:
        """Trigger async conversation compaction.

        Compaction runs asynchronously on the server. Use
        get_compaction_status() to poll for completion, then
        get_compacted() to retrieve the result.

        Args:
            conversation_id: The conversation to compact
            strategy: Override strategy (aggressive, balanced, conservative, adaptive)
            compaction_level: Backward-compatible alias for strategy
            target_tokens: Override target token count
            force: Compact even if under threshold

        Returns:
            CompactionTriggerResponse with compaction_id and status
        """
        self._sdk._ensure_initialized()

        correlation_id = generate_correlation_id(self._sdk.instance_id)
        start_time = datetime.now(timezone.utc)

        auth_context = await self._sdk._get_auth_context(correlation_id)

        resolved_strategy = strategy or compaction_level

        result = await self._sdk._http_transport.post(
            "/v1/conversations/compact",
            auth_context=auth_context,
            json={
                "conversation_id": conversation_id,
                "strategy": resolved_strategy,
                "target_tokens": target_tokens,
                "force": force,
            },
            correlation_id=correlation_id,
        )

        # Emit telemetry
        latency_ms = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000)
        from .telemetry.models import TelemetryEventType
        self._sdk._telemetry_collector.emit(
            event_type=TelemetryEventType.COMPACT_CONTEXT,
            correlation_id=correlation_id,
            latency_ms=latency_ms,
            scope="conversation",
            status="success",
        )

        response = CompactionTriggerResponse(
            compaction_id=result.get("compaction_id", "pending"),
            conversation_id=result.get("conversation_id", conversation_id),
            status=result.get("status", "in_progress"),
            trigger_type=result.get("trigger_type", "manual_api"),
            initiated_at=result.get("initiated_at", datetime.now(timezone.utc)),
            estimated_completion_seconds=result.get("estimated_completion_seconds", 60),
            previous_context=result.get("previous_context"),
            previous_context_age_seconds=result.get("previous_context_age_seconds"),
            previous_compaction_id=result.get("previous_compaction_id"),
        )

        # Cache previous_context locally so get_compacted() can serve it
        if response.previous_context:
            try:
                self._sdk._cache_manager.set(
                    scope=CacheScope.CONVERSATION,
                    entity_id=conversation_id,
                    context_type="compacted_full",
                    value=json.dumps(response.previous_context).encode(),
                    ttl_seconds=300,
                    query={"format": "structured"},
                )
            except Exception:
                pass  # Non-critical

        return response

    async def get_compacted(
        self,
        conversation_id: str,
        version: Optional[int] = None,
        format: str = "structured",
    ) -> Optional[CompactionResponse]:
        """Get existing compacted context without triggering new compaction.

        Checks local SDK cache first (unless a specific version is requested).
        On cache miss, fetches from cloud and caches the result for 5 minutes.

        Args:
            conversation_id: ID of the conversation
            version: Specific version number (latest if not specified).
                     When specified, always fetches from cloud (skip cache).
            format: Output format (structured, narrative, injection)

        Returns:
            CompactionResponse if exists, None otherwise
        """
        self._sdk._ensure_initialized()

        correlation_id = generate_correlation_id(self._sdk.instance_id)

        # Check local cache first (skip when version is pinned)
        if version is None:
            try:
                cached = self._sdk._cache_manager.get(
                    scope=CacheScope.CONVERSATION,
                    entity_id=conversation_id,
                    context_type="compacted_full",
                    query={"format": format},
                )
                if cached:
                    return CompactionResponse(**json.loads(cached))
            except Exception:
                pass  # Cache miss or error — fall through to cloud fetch

        try:
            auth_context = await self._sdk._get_auth_context(correlation_id)

            params = {"format": format}
            if version is not None:
                params["version"] = version

            result = await self._sdk._http_transport.get(
                f"/v1/conversations/{conversation_id}/compacted",
                auth_context=auth_context,
                params=params,
                correlation_id=correlation_id,
            )

            metadata = ResponseMetadata(
                correlation_id=correlation_id,
                ttl_seconds=result.get("ttl_seconds", 300),
                source="cloud",
                retrieved_at=datetime.now(timezone.utc),
            )

            response = CompactionResponse(
                compacted_context=result.get("formatted_context", ""),
                original_token_count=result.get("original_token_count", 0),
                compacted_token_count=result.get("compacted_token_count", 0),
                compression_ratio=result.get("compression_ratio", 0.0),
                level_applied=CompactionLevel(result.get("strategy_used", "adaptive")),
                metadata=metadata,
                compaction_id=result.get("compaction_id"),
                strategy_used=result.get("strategy_used"),
                validation_score=result.get("validation_score"),
                validation_passed=result.get("validation_passed"),
                facts=result.get("facts", []),
                decisions=result.get("decisions", []),
                preferences=result.get("preferences", []),
                current_state=result.get("current_state"),
                quality_warning=result.get("quality_warning"),
            )

            # Cache the response locally (5 min TTL)
            try:
                self._sdk._cache_manager.set(
                    scope=CacheScope.CONVERSATION,
                    entity_id=conversation_id,
                    context_type="compacted_full",
                    value=response.model_dump_json().encode(),
                    ttl_seconds=300,
                    query={"format": format},
                )
            except Exception:
                pass  # Non-critical — caching is best-effort

            return response

        except ContextNotFoundError:
            return None

    async def get_compaction_status(
        self,
        conversation_id: str,
    ) -> CompactionStatusResponse:
        """Get compaction status for a conversation.

        Checks the anticipation cache first — if a ``compaction_update``
        bundle was received via gRPC for this conversation, returns
        ``status="completed"`` immediately without a cloud round-trip.

        Returns info about:
        - Whether compacted context exists
        - Whether it's stale
        - Whether compaction is in progress

        Args:
            conversation_id: ID of the conversation

        Returns:
            CompactionStatusResponse with status information
        """
        self._sdk._ensure_initialized()

        # Check anticipation cache for a compaction_update bundle
        try:
            for entry in self._sdk._anticipation_cache._entries.values():
                if (
                    entry.bundle_type == "compaction_update"
                    and entry.conversation_id == conversation_id
                ):
                    ctx = entry.bundle.get("conversation_context", {})
                    return CompactionStatusResponse(
                        conversation_id=conversation_id,
                        status="completed",
                        compaction_id=ctx.get("compaction_id"),
                        completed_at=ctx.get("compacted_at"),
                    )
        except Exception:
            pass  # Fall through to cloud fetch

        correlation_id = generate_correlation_id(self._sdk.instance_id)
        auth_context = await self._sdk._get_auth_context(correlation_id)

        result = await self._sdk._http_transport.get(
            f"/v1/conversations/{conversation_id}/compaction/status",
            auth_context=auth_context,
            correlation_id=correlation_id,
        )

        return CompactionStatusResponse(**result)

    async def get_context_for_prompt(
        self,
        conversation_id: str,
        style: str = "structured",
    ) -> ContextForPromptResponse:
        """Get compacted context + recent un-compacted messages for LLM prompt injection.

        Returns a single ``formatted_context`` string that combines the compacted
        history with any messages that arrived after the last compaction cutoff,
        ready to inject into an LLM prompt. Also provides raw ``recent_messages``
        for custom formatting.

        If no compaction exists yet, all conversation messages are returned as
        recent messages so the method is useful even before the first compaction.

        Args:
            conversation_id: ID of the conversation
            style: Formatting style - "structured", "narrative", or "bullet_points"

        Returns:
            ContextForPromptResponse with formatted_context, recent_messages, and metadata
        """
        self._sdk._ensure_initialized()

        # Check local cache first (Tier 2 optimization)
        try:
            cached = self._sdk._cache_manager.get(
                scope=CacheScope.CONVERSATION,
                entity_id=conversation_id,
                context_type="compacted_context",
                query={"style": style},
            )
            if cached:
                import json as _json
                return ContextForPromptResponse(**_json.loads(cached))
        except Exception:
            pass  # Cache miss or error — fall through to cloud fetch

        # Fetch from new cloud endpoint (compacted + recent messages combined)
        correlation_id = generate_correlation_id(self._sdk.instance_id)
        auth_context = await self._sdk._get_auth_context(correlation_id)

        result = await self._sdk._http_transport.get(
            f"/v1/conversations/{conversation_id}/context-for-prompt",
            params={"style": style},
            auth_context=auth_context,
            correlation_id=correlation_id,
        )

        response = ContextForPromptResponse(
            formatted_context=result.get("formatted_context") or None,
            available=result.get("available", False),
            is_stale=result.get("is_stale", False),
            compression_ratio=result.get("compression_ratio"),
            validation_score=result.get("validation_score"),
            compaction_age_seconds=result.get("compaction_age_seconds"),
            quality_warning=result.get("quality_warning", False),
            recent_messages=result.get("recent_messages", []),
            recent_message_count=result.get("recent_message_count", 0),
            compacted_message_count=result.get("compacted_message_count", 0),
            total_message_count=result.get("total_message_count", 0),
        )

        # Cache the response locally (5 min TTL)
        try:
            self._sdk._cache_manager.set(
                scope=CacheScope.CONVERSATION,
                entity_id=conversation_id,
                context_type="compacted_context",
                value=response.model_dump_json().encode(),
                ttl_seconds=300,
                query={"style": style},
            )
        except Exception:
            pass  # Non-critical — caching is best-effort

        return response


class UserInterface:
    """Interface for user-scoped operations."""

    def __init__(self, sdk: MaximemSynapSDK):
        self._sdk = sdk
        self.context = UserContextInterface(sdk)


class UserContextInterface:
    """User context operations."""

    def __init__(self, sdk: MaximemSynapSDK):
        self._sdk = sdk

    async def fetch(
        self,
        user_id: str,
        conversation_id: Optional[str] = None,
        search_query: Optional[List[str]] = None,
        max_results: int = 10,
        types: Optional[List[str]] = None,
        mode: str = "fast",
        customer_id: Optional[str] = None,
    ) -> ContextResponse:
        """Fetch context for a user.

        Args:
            user_id: The user to fetch context for
            conversation_id: Optional conversation for relevance
            search_query: Optional search queries
            max_results: Maximum results to return
            types: Context types to include
            mode: Retrieval mode - "fast" (default) or "accurate"
            customer_id: Optional customer ID. Required for B2B instances.
                For B2C instances, this is auto-resolved from user_id.

        Returns:
            ContextResponse with user facts, preferences, etc.
        """
        self._sdk._ensure_initialized()

        # Validate mode
        valid_modes = ("fast", "accurate")
        if mode not in valid_modes:
            raise InvalidInputError(f"Invalid mode '{mode}'. Must be one of: {valid_modes}")

        correlation_id = generate_correlation_id(self._sdk.instance_id)
        start_time = datetime.now(timezone.utc)
        cache_hit = False

        cache_params = {
            "conversation_id": conversation_id,
            "search_query": search_query,
            "max_results": max_results,
            "types": types,
            "mode": mode,
        }

        # Check anticipation cache first (bundles pre-fetched via gRPC stream)
        anticipated = self._sdk._anticipation_cache.lookup(
            search_query=search_query,
            entity_id=user_id,
        )
        if anticipated:
            response = _build_anticipation_response(
                anticipated, correlation_id, start_time,
                scope="user", mode=mode,
                telemetry_collector=self._sdk._telemetry_collector,
            )
        else:
            # Check local cache
            cached = self._sdk._cache_manager.get(
                scope=CacheScope.USER,
                entity_id=user_id,
                context_type="context",
                query=cache_params,
            )

            if cached:
                cache_hit = True
                data = json.loads(cached)
                metadata = ResponseMetadata(
                    correlation_id=correlation_id,
                    ttl_seconds=0,
                    source="cache",
                    retrieved_at=datetime.now(timezone.utc),
                )
                response = ContextResponse.from_cloud_response(data, metadata)
            else:
                auth_context = await self._sdk._get_auth_context(correlation_id)

                result = await self._sdk._http_transport.post(
                    "/v1/context/user/fetch",
                    auth_context=auth_context,
                    json={
                        "user_id": user_id,
                        "customer_id": customer_id,
                        "conversation_id": conversation_id,
                        "search_query": search_query,
                        "max_results": max_results,
                        "types": types or ["all"],
                        "mode": mode,
                    },
                    correlation_id=correlation_id,
                )

                metadata = ResponseMetadata(
                    correlation_id=correlation_id,
                    ttl_seconds=result.get("ttl_seconds", 300),
                    source="cloud",
                    retrieved_at=datetime.now(timezone.utc),
                )
                response = ContextResponse.from_cloud_response(result.get("context", {}), metadata)

                # Cache (skip empty responses to avoid caching transient failures)
                context_data = result.get("context", {})
                if any(context_data.get(k) for k in ("facts", "preferences", "episodes", "emotions", "temporal_events")):
                    self._sdk._cache_manager.set(
                        scope=CacheScope.USER,
                        entity_id=user_id,
                        context_type="context",
                        value=json.dumps(context_data).encode(),
                        ttl_seconds=metadata.ttl_seconds,
                        query=cache_params,
                    )

            latency_ms = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000)
            cache_origin = "local_http_cache" if cache_hit else "cloud_fetch"
            emit_fetch_event(
                self._sdk._telemetry_collector,
                scope="user",
                correlation_id=correlation_id,
                latency_ms=latency_ms,
                cache_hit=cache_hit,
                mode=mode,
                cache_origin=cache_origin,
            )

        # Auto-emit context_fetch for server-side retrieval history
        items_count = len(response.facts) + len(response.preferences) + len(response.episodes) + len(response.emotions)
        asyncio.ensure_future(_emit_context_fetch_event(
            sdk=self._sdk,
            scope="user",
            search_query=search_query,
            types=types,
            mode=mode,
            source=response.metadata.source,
            items_count=items_count,
            conversation_id=conversation_id or "",
            user_id=user_id,
        ))

        # Periodic user summary injection
        turn = self._sdk._increment_turn(conversation_id)
        if self._sdk._should_inject_user_summary(conversation_id):
            summary = self._sdk._anticipation_cache.lookup_user_summary(
                entity_id=user_id,
            )
            if summary:
                response = _merge_user_summary_into_response(response, summary)
                logger.debug("Injected user summary at turn %d for user %s", turn, user_id)

        return response


class CustomerInterface:
    """Interface for customer-scoped operations."""

    def __init__(self, sdk: MaximemSynapSDK):
        self._sdk = sdk
        self.context = CustomerContextInterface(sdk)


class CustomerContextInterface:
    """Customer context operations."""

    def __init__(self, sdk: MaximemSynapSDK):
        self._sdk = sdk

    async def fetch(
        self,
        customer_id: str,
        conversation_id: Optional[str] = None,
        search_query: Optional[List[str]] = None,
        max_results: int = 10,
        types: Optional[List[str]] = None,
        mode: str = "fast",
    ) -> ContextResponse:
        """Fetch context for a customer (B2B)."""
        self._sdk._ensure_initialized()

        # Validate mode
        valid_modes = ("fast", "accurate")
        if mode not in valid_modes:
            raise InvalidInputError(f"Invalid mode '{mode}'. Must be one of: {valid_modes}")

        correlation_id = generate_correlation_id(self._sdk.instance_id)
        start_time = datetime.now(timezone.utc)
        cache_hit = False

        cache_params = {
            "conversation_id": conversation_id,
            "search_query": search_query,
            "max_results": max_results,
            "types": types,
            "mode": mode,
        }

        # Check anticipation cache first (bundles pre-fetched via gRPC stream)
        anticipated = self._sdk._anticipation_cache.lookup(
            search_query=search_query,
            entity_id=customer_id,
        )
        if anticipated:
            response = _build_anticipation_response(
                anticipated, correlation_id, start_time,
                scope="customer", mode=mode,
                telemetry_collector=self._sdk._telemetry_collector,
            )
        else:
            cached = self._sdk._cache_manager.get(
                scope=CacheScope.CUSTOMER,
                entity_id=customer_id,
                context_type="context",
                query=cache_params,
            )

            if cached:
                cache_hit = True
                data = json.loads(cached)
                metadata = ResponseMetadata(
                    correlation_id=correlation_id,
                    ttl_seconds=0,
                    source="cache",
                    retrieved_at=datetime.now(timezone.utc),
                )
                response = ContextResponse.from_cloud_response(data, metadata)
            else:
                auth_context = await self._sdk._get_auth_context(correlation_id)

                result = await self._sdk._http_transport.post(
                    "/v1/context/customer/fetch",
                    auth_context=auth_context,
                    json={
                        "customer_id": customer_id,
                        "conversation_id": conversation_id,
                        "search_query": search_query,
                        "max_results": max_results,
                        "types": types or ["all"],
                        "mode": mode,
                    },
                    correlation_id=correlation_id,
                )

                metadata = ResponseMetadata(
                    correlation_id=correlation_id,
                    ttl_seconds=result.get("ttl_seconds", 300),
                    source="cloud",
                    retrieved_at=datetime.now(timezone.utc),
                )
                response = ContextResponse.from_cloud_response(result.get("context", {}), metadata)

                # Skip caching empty responses to avoid caching transient failures
                context_data = result.get("context", {})
                if any(context_data.get(k) for k in ("facts", "preferences", "episodes", "emotions", "temporal_events")):
                    self._sdk._cache_manager.set(
                        scope=CacheScope.CUSTOMER,
                        entity_id=customer_id,
                        context_type="context",
                        value=json.dumps(context_data).encode(),
                        ttl_seconds=metadata.ttl_seconds,
                        query=cache_params,
                    )

            latency_ms = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000)
            cache_origin = "local_http_cache" if cache_hit else "cloud_fetch"
            emit_fetch_event(
                self._sdk._telemetry_collector,
                scope="customer",
                correlation_id=correlation_id,
                latency_ms=latency_ms,
                cache_hit=cache_hit,
                mode=mode,
                cache_origin=cache_origin,
            )

        # Auto-emit context_fetch for server-side retrieval history
        items_count = len(response.facts) + len(response.preferences) + len(response.episodes) + len(response.emotions)
        asyncio.ensure_future(_emit_context_fetch_event(
            sdk=self._sdk,
            scope="customer",
            search_query=search_query,
            types=types,
            mode=mode,
            source=response.metadata.source,
            items_count=items_count,
            conversation_id=conversation_id or "",
            customer_id=customer_id,
        ))

        # Periodic user summary injection
        turn = self._sdk._increment_turn(conversation_id)
        if self._sdk._should_inject_user_summary(conversation_id):
            summary = self._sdk._anticipation_cache.lookup_user_summary(
                entity_id=customer_id,
            )
            if summary:
                response = _merge_user_summary_into_response(response, summary)
                logger.debug("Injected user summary at turn %d for customer %s", turn, customer_id)

        return response


class ClientInterface:
    """Interface for client (org) scoped operations."""

    def __init__(self, sdk: MaximemSynapSDK):
        self._sdk = sdk
        self.context = ClientContextInterface(sdk)


class ClientContextInterface:
    """Client/org context operations."""

    def __init__(self, sdk: MaximemSynapSDK):
        self._sdk = sdk

    async def fetch(
        self,
        conversation_id: Optional[str] = None,
        search_query: Optional[List[str]] = None,
        max_results: int = 10,
        types: Optional[List[str]] = None,
        mode: str = "fast",
    ) -> ContextResponse:
        """Fetch organizational context."""
        self._sdk._ensure_initialized()

        # Validate mode
        valid_modes = ("fast", "accurate")
        if mode not in valid_modes:
            raise InvalidInputError(f"Invalid mode '{mode}'. Must be one of: {valid_modes}")

        correlation_id = generate_correlation_id(self._sdk.instance_id)
        start_time = datetime.now(timezone.utc)
        cache_hit = False

        cache_params = {
            "conversation_id": conversation_id,
            "search_query": search_query,
            "max_results": max_results,
            "types": types,
            "mode": mode,
        }

        # Check anticipation cache first (bundles pre-fetched via gRPC stream)
        anticipated = self._sdk._anticipation_cache.lookup(
            search_query=search_query,
            entity_id="_client",  # sentinel — prevents matching user/customer scoped bundles
        )
        if anticipated:
            response = _build_anticipation_response(
                anticipated, correlation_id, start_time,
                scope="client", mode=mode,
                telemetry_collector=self._sdk._telemetry_collector,
            )
        else:
            cached = self._sdk._cache_manager.get(
                scope=CacheScope.CLIENT,
                entity_id=self._sdk._client_id,
                context_type="context",
                query=cache_params,
            )

            if cached:
                cache_hit = True
                data = json.loads(cached)
                metadata = ResponseMetadata(
                    correlation_id=correlation_id,
                    ttl_seconds=0,
                    source="cache",
                    retrieved_at=datetime.now(timezone.utc),
                )
                response = ContextResponse.from_cloud_response(data, metadata)
            else:
                auth_context = await self._sdk._get_auth_context(correlation_id)

                result = await self._sdk._http_transport.post(
                    "/v1/context/client/fetch",
                    auth_context=auth_context,
                    json={
                        "conversation_id": conversation_id,
                        "search_query": search_query,
                        "max_results": max_results,
                        "types": types or ["all"],
                        "mode": mode,
                    },
                    correlation_id=correlation_id,
                )

                metadata = ResponseMetadata(
                    correlation_id=correlation_id,
                    ttl_seconds=result.get("ttl_seconds", 1800),  # 30 min for client
                    source="cloud",
                    retrieved_at=datetime.now(timezone.utc),
                )
                response = ContextResponse.from_cloud_response(result.get("context", {}), metadata)

                # Skip caching empty responses to avoid caching transient failures
                context_data = result.get("context", {})
                if any(context_data.get(k) for k in ("facts", "preferences", "episodes", "emotions", "temporal_events")):
                    self._sdk._cache_manager.set(
                        scope=CacheScope.CLIENT,
                        entity_id=self._sdk._client_id,
                        context_type="context",
                        value=json.dumps(context_data).encode(),
                        ttl_seconds=metadata.ttl_seconds,
                        query=cache_params,
                    )

            latency_ms = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000)
            cache_origin = "local_http_cache" if cache_hit else "cloud_fetch"
            emit_fetch_event(
                self._sdk._telemetry_collector,
                scope="client",
                correlation_id=correlation_id,
                latency_ms=latency_ms,
                cache_hit=cache_hit,
                mode=mode,
                cache_origin=cache_origin,
            )

        # Auto-emit context_fetch for server-side retrieval history
        items_count = len(response.facts) + len(response.preferences) + len(response.episodes) + len(response.emotions)
        asyncio.ensure_future(_emit_context_fetch_event(
            sdk=self._sdk,
            scope="client",
            search_query=search_query,
            types=types,
            mode=mode,
            source=response.metadata.source,
            items_count=items_count,
            conversation_id=conversation_id or "",
        ))

        # Periodic user summary injection
        turn = self._sdk._increment_turn(conversation_id)
        if self._sdk._should_inject_user_summary(conversation_id):
            summary = self._sdk._anticipation_cache.lookup_user_summary(
                entity_id="_client",
            )
            if summary:
                response = _merge_user_summary_into_response(response, summary)
                logger.debug("Injected user summary at turn %d for client scope", turn)

        return response


class InstanceInterface:
    """Interface for instance-level operations (listening).

    Wires SDK components into InstanceController and delegates.
    """

    def __init__(self, sdk: MaximemSynapSDK):
        self._sdk = sdk
        self._controller = InstanceController(
            transport_factory=self._create_transport,
            auth_provider=self._sdk._get_auth_context,
        )

    def _create_transport(self, **kwargs) -> GRPCTransport:
        """Factory that creates a GRPCTransport with SDK configuration."""
        transport = GRPCTransport(
            instance_id=self._sdk.instance_id,
            host=self._sdk._config.grpc_host,
            port=self._sdk._config.grpc_port,
            use_tls=self._sdk._config.grpc_use_tls,
            timeouts=self._sdk._config.timeouts,
            telemetry_callback=self._sdk._on_telemetry_event,
            **kwargs,
        )
        self._sdk._grpc_transport = transport
        return transport

    async def listen(
        self,
        on_reconnect: Optional[Callable[[int], None]] = None,
        on_disconnect: Optional[Callable[[str], None]] = None,
        on_context: Optional[Callable[[Dict[str, Any]], None]] = None,
    ) -> None:
        """Start listening to agent activity.

        Establishes a bidirectional gRPC stream for real-time
        context anticipation.

        Args:
            on_reconnect: Callback when stream reconnects (receives attempt count)
            on_disconnect: Callback when stream disconnects (receives reason)
            on_context: Callback when an anticipated context bundle arrives.
                The bundle dict contains items_by_type, retrieval_mode, etc.
                Bundles are also stored in the SDK's anticipation cache
                automatically, so fetch() can find them without a round-trip.
        """
        self._sdk._ensure_initialized()
        self._on_context_callback = on_context
        await self._controller.listen(
            on_reconnect=on_reconnect,
            on_disconnect=on_disconnect,
            on_message=self._handle_anticipated_bundle,
        )

    def _handle_anticipated_bundle(self, bundle_dict: Dict[str, Any]) -> None:
        """Handle an anticipated context bundle from the server.

        Stores anticipation, user_summary, and compaction_update bundles in
        the SDK's anticipation cache. Reactive bundles (from direct retrieval)
        are skipped — they are immediate results, not predictions.

        For compaction_update bundles, also invalidates stale local cache
        entries so get_compacted() and get_context_for_prompt() fetch fresh
        data on next call.
        """
        bundle_type = bundle_dict.get("_bundle_type", "anticipation")

        if bundle_type != "reactive":
            self._sdk._anticipation_cache.store(bundle_dict)
        else:
            logger.debug(
                "Skipping reactive bundle from AnticipationCache: %s",
                bundle_dict.get("bundle_id"),
            )

        # Invalidate stale local cache on compaction_update
        if bundle_type == "compaction_update":
            conv_id = bundle_dict.get("_anticipation_conversation_id")
            if conv_id:
                try:
                    self._sdk._cache_manager.delete(
                        scope=CacheScope.CONVERSATION,
                        entity_id=conv_id,
                        context_type="compacted_full",
                    )
                    self._sdk._cache_manager.delete(
                        scope=CacheScope.CONVERSATION,
                        entity_id=conv_id,
                        context_type="compacted_context",
                    )
                    logger.debug(
                        "Invalidated local compaction cache for conversation %s",
                        conv_id,
                    )
                except Exception:
                    pass  # Non-critical

        # Always invoke user callback regardless of type
        if hasattr(self, "_on_context_callback") and self._on_context_callback:
            try:
                if asyncio.iscoroutinefunction(self._on_context_callback):
                    asyncio.create_task(self._on_context_callback(bundle_dict))
                else:
                    self._on_context_callback(bundle_dict)
            except Exception as e:
                logger.warning(f"on_context callback error: {e}")

    async def stop_listening(self) -> None:
        """Stop listening to agent activity."""
        await self._controller.stop()
        self._sdk._grpc_transport = None

    async def send_message(
        self,
        content: str,
        role: str = "user",
        conversation_id: Optional[str] = None,
        user_id: Optional[str] = None,
        customer_id: Optional[str] = None,
        session_id: Optional[str] = None,
        event_type: str = "user_message",
        metadata: Optional[Dict[str, str]] = None,
    ) -> None:
        """Send a conversation message over the active gRPC stream.

        Args:
            content: Message content
            role: "user" or "assistant"
            conversation_id: Conversation identifier
            user_id: User identifier
            customer_id: Customer identifier
            session_id: Session identifier
            event_type: Event type (user_message, assistant_message, tool_call, etc.)
            metadata: Additional string key-value metadata

        Raises:
            ListeningNotActiveError: If listen() has not been called.
        """
        from .models.errors import ListeningNotActiveError

        if not self.is_listening:
            raise ListeningNotActiveError()

        await self._controller._transport.send({
            "event_type": event_type,
            "content": content,
            "role": role,
            "conversation_id": conversation_id or "",
            "user_id": user_id or "",
            "customer_id": customer_id or "",
            "session_id": session_id or "",
            "metadata": metadata or {},
        })

    @property
    def is_listening(self) -> bool:
        """Check if currently listening."""
        return self._controller.is_listening


class CacheInterface:
    """Interface for cache management."""

    def __init__(self, sdk: MaximemSynapSDK):
        self._sdk = sdk

    def clear(self) -> None:
        """Clear all cached data."""
        if self._sdk._cache_manager:
            self._sdk._cache_manager.clear_all()

    def clear_user(self, user_id: str) -> None:
        """Clear cached data for a specific user (GDPR)."""
        if self._sdk._cache_manager:
            self._sdk._cache_manager.clear_user(user_id)

    def clear_customer(self, customer_id: str) -> None:
        """Clear cached data for a specific customer."""
        if self._sdk._cache_manager:
            self._sdk._cache_manager.clear_customer(customer_id)

    def stats(self) -> Dict[str, Any]:
        """Get cache statistics."""
        if self._sdk._cache_manager:
            return self._sdk._cache_manager.stats()
        return {"enabled": False}
