"""
High-Level Agent Context Interface

This module provides a simplified, generic high-level interface for agent context management,
RAG, and GraphRAG. It uses generic method names (store, retrieve, forget, conversation) that
auto-detect content types and retrieval strategies, with boolean flags for common options.

Key Features:
    - Generic methods: store(), retrieve(), forget(), conversation()
    - Auto-detection: Memory vs documents, RAG vs GraphRAG
    - Boolean flags: Simple True/False parameters for common options
    - User-friendly: Easy to use interface for agentic systems

Example Usage:
    >>> from semantica.context import AgentContext
    >>> context = AgentContext(vector_store=vs, knowledge_graph=kg)
    >>> memory_id = context.store("User likes Python", conversation_id="conv1")
    >>> results = context.retrieve("Python programming")
    >>> stats = context.store(["Doc 1", "Doc 2"], extract_entities=True)
"""

from datetime import datetime
from typing import Any, Dict, List, Optional, Union

from ..utils.exceptions import ProcessingError, ValidationError
from ..utils.logging import get_logger
from .agent_memory import AgentMemory
from .context_graph import ContextGraph
from .context_retriever import ContextRetriever, RetrievedContext
from .entity_linker import EntityLinker


class AgentContext:
    """
    High-level interface for agent context management, RAG, and GraphRAG.
    
    Provides generic methods (store, retrieve, forget, conversation) that auto-detect
    content types and retrieval strategies. Uses boolean flags for common options.
    
    Attributes:
        vector_store: Vector store instance
        knowledge_graph: Knowledge graph instance (optional)
        memory: AgentMemory instance (via property)
        retriever: ContextRetriever instance (via property, if knowledge_graph available)
        graph_builder: ContextGraph instance (via property, if knowledge_graph supports building)
    
    Main Methods:
        - store(): Store memory or documents (auto-detects type)
        - retrieve(): Retrieve context (auto-detects RAG vs GraphRAG)
        - forget(): Delete memories
        - conversation(): Get conversation history
        - get_memory(): Get specific memory by ID
        - stats(): Get memory statistics
        - link(): Link entities in text
        - build_graph(): Build context graph manually
    
    Example:
        >>> context = AgentContext(vector_store=vs, knowledge_graph=kg)
        >>> memory_id = context.store("User likes Python", conversation_id="conv1")
        >>> results = context.retrieve("Python programming")
        >>> stats = context.stats()
        >>> memory = context.get_memory(memory_id)
    """

    def __init__(
        self,
        vector_store: Any,
        knowledge_graph: Optional[Any] = None,
        retention_days: Optional[int] = 30,
        max_memories: int = 10000,
        use_graph_expansion: bool = True,
        max_expansion_hops: int = 2,
        hybrid_alpha: float = 0.5,
        **kwargs
    ):
        """
        Initialize AgentContext.
        
        Args:
            vector_store: Vector store instance (required)
            knowledge_graph: Knowledge graph instance (optional, enables GraphRAG)
            retention_days: Days to keep memories (default: 30, None=unlimited)
            max_memories: Maximum number of memories (default: 10000)
            use_graph_expansion: Enable graph expansion for retrieval (default: True)
            max_expansion_hops: Maximum hops for graph expansion (default: 2)
            hybrid_alpha: Balance between vector (0) and graph (1) retrieval (default: 0.5)
            **kwargs: Additional options passed to underlying components
        
        Raises:
            ValueError: If vector_store is not provided
        """
        self.logger = get_logger("agent_context")
        
        if vector_store is None:
            raise ValueError(
                "vector_store is required for AgentContext. "
                "Please provide a vector store instance. "
                "Example: AgentContext(vector_store=your_vector_store)"
            )
        
        self.vector_store = vector_store
        self.knowledge_graph = knowledge_graph
        
        # Initialize AgentMemory
        retention_policy = f"{retention_days}_days" if retention_days else "unlimited"
        memory_config = {
            "vector_store": vector_store,
            "knowledge_graph": knowledge_graph,
            "retention_policy": retention_policy,
            "max_memory_size": max_memories,
            **kwargs
        }
        self._memory = AgentMemory(**memory_config)
        
        # Initialize ContextRetriever if knowledge_graph available
        if knowledge_graph:
            retriever_config = {
                "memory_store": self._memory,
                "knowledge_graph": knowledge_graph,
                "vector_store": vector_store,
                "use_graph_expansion": use_graph_expansion,
                "max_expansion_hops": max_expansion_hops,
                "hybrid_alpha": hybrid_alpha,
                **kwargs
            }
            self._retriever = ContextRetriever(**retriever_config)
        else:
            self._retriever = None
        
        # Initialize ContextGraphBuilder if knowledge_graph available and supports building
        self._graph_builder = None
        if knowledge_graph and hasattr(knowledge_graph, "build_from_conversations"):
             self._graph_builder = knowledge_graph
        
        # Store config
        self.config = {
            "retention_days": retention_days,
            "max_memories": max_memories,
            "use_graph_expansion": use_graph_expansion,
            "max_expansion_hops": max_expansion_hops,
            "hybrid_alpha": hybrid_alpha,
        }

    @property
    def memory(self) -> AgentMemory:
        """Access underlying AgentMemory instance."""
        return self._memory

    @property
    def retriever(self) -> Optional[ContextRetriever]:
        """Access underlying ContextRetriever instance."""
        return self._retriever

    @property
    def graph_builder(self) -> Optional[Any]:
        """Access underlying ContextGraph instance for building."""
        return self._graph_builder

    def store(
        self,
        content: Union[str, List[str], List[Dict[str, Any]]],
        metadata: Optional[Dict[str, Any]] = None,
        conversation_id: Optional[str] = None,
        user_id: Optional[str] = None,
        entities: Optional[List[Dict[str, Any]]] = None,
        relationships: Optional[List[Dict[str, Any]]] = None,
        extract_entities: bool = True,
        extract_relationships: bool = True,
        link_entities: bool = True,
        auto_extract: bool = False,
        **kwargs
    ) -> Union[str, Dict[str, Any]]:
        """
        Store content (memory or documents).
        
        Auto-detects content type:
        - Single string -> Memory item
        - List of strings/dicts -> Documents (builds graph if knowledge_graph available)
        
        Args:
            content: Content to store:
                - str: Single memory item
                - List[str]: Multiple documents
                - List[Dict]: Documents with metadata
            metadata: Additional metadata dict
            conversation_id: Conversation ID (for memories)
            user_id: User ID (for memories)
            entities: Pre-extracted entities (optional)
            relationships: Pre-extracted relationships (optional)
            extract_entities: Extract entities from documents (default: True)
            extract_relationships: Extract relationships (default: True)
            link_entities: Link entities across documents (default: True)
            auto_extract: Auto-extract entities/relationships if not provided (default: False)
            **kwargs: Additional options passed to storage methods
        
        Returns:
            - str: Memory ID (for single memory)
            - Dict: Statistics (for documents) with keys: stored_count, graph_nodes, graph_edges
        
        Example:
            >>> # Store memory
            >>> memory_id = context.store("User likes Python", conversation_id="conv1")
            
            >>> # Store documents
            >>> stats = context.store(["Doc 1", "Doc 2"], extract_entities=True)
        """
        # Auto-detect content type
        if isinstance(content, str):
            # Single memory item
            memory_metadata = metadata or {}
            if conversation_id:
                memory_metadata["conversation_id"] = conversation_id
            if user_id:
                memory_metadata["user_id"] = user_id
            
            return self._memory.store(
                content,
                metadata=memory_metadata,
                entities=entities,
                relationships=relationships,
                **kwargs
            )
        
        elif isinstance(content, list):
            # Documents - normalize to list of dicts
            documents = self._normalize_documents(content)
            
            # Add entities/relationships to documents if provided
            if entities or relationships:
                for doc in documents:
                    if entities and "entities" not in doc:
                        doc["entities"] = entities
                    if relationships and "relationships" not in doc:
                        doc["relationships"] = relationships
            
            # Store each document in vector store
            stored_ids = []
            for doc in documents:
                doc_metadata = {**(metadata or {}), **doc.get("metadata", {})}
                doc_entities = doc.get("entities")
                doc_relationships = doc.get("relationships")
                
                doc_id = self._memory.store(
                    doc["content"],
                    metadata=doc_metadata,
                    entities=doc_entities,
                    relationships=doc_relationships,
                    **kwargs
                )
                stored_ids.append(doc_id)
            
            # Build graph if knowledge_graph available and flags set
            graph_stats = {}
            if self.knowledge_graph and (extract_entities or extract_relationships or auto_extract):
                graph_stats = self._build_graph_from_documents(
                    documents,
                    extract_entities=extract_entities or auto_extract,
                    extract_relationships=extract_relationships or auto_extract,
                    link_entities=link_entities
                )
            
            return {
                "stored_count": len(stored_ids),
                "memory_ids": stored_ids,
                "graph_nodes": graph_stats.get("node_count", 0),
                "graph_edges": graph_stats.get("edge_count", 0),
            }
        
        else:
            raise ValueError(
                f"Unsupported content type: {type(content)}. "
                "Expected str (single memory) or list (multiple documents). "
                f"Received: {type(content).__name__}"
            )

    def retrieve(
        self,
        query: str,
        max_results: int = 5,
        use_graph: Optional[bool] = None,
        min_score: float = 0.0,
        conversation_id: Optional[str] = None,
        user_id: Optional[str] = None,
        include_entities: bool = True,
        include_relationships: bool = False,
        expand_graph: bool = True,
        deduplicate: bool = True,
        **kwargs
    ) -> List[Dict[str, Any]]:
        """
        Retrieve relevant context.
        
        Auto-detects best strategy:
        - If knowledge_graph available -> GraphRAG (hybrid retrieval)
        - Otherwise -> Simple RAG (vector only)
        
        Args:
            query: Search query
            max_results: Maximum results (default: 5)
            use_graph: Force graph usage (None=auto-detect, True=force, False=vector only)
            min_score: Minimum relevance score (default: 0.0)
            conversation_id: Filter by conversation ID
            user_id: Filter by user ID
            include_entities: Include related entities in results (default: True)
            include_relationships: Include relationships in results (default: False)
            expand_graph: Use graph expansion (default: True)
            deduplicate: Deduplicate results (default: True)
            **kwargs: Additional filters (type, date_range, etc.)
        
        Returns:
            List of context dicts with keys: content, score, source, metadata, related_entities
        
        Example:
            >>> # Auto-detects RAG vs GraphRAG
            >>> results = context.retrieve("Python programming")
            
            >>> # Force vector-only retrieval
            >>> results = context.retrieve("Python", use_graph=False)
        """
        # Auto-detect strategy
        if use_graph is None:
            use_graph = (self.knowledge_graph is not None and self._retriever is not None)
        
        # Apply filters if provided
        if conversation_id:
            kwargs["conversation_id"] = conversation_id
        if user_id:
            kwargs["user_id"] = user_id
        
        if use_graph and self._retriever:
            # GraphRAG: Use ContextRetriever (hybrid retrieval)
            results = self._retriever.retrieve(
                query,
                max_results=max_results,
                use_graph_expansion=expand_graph,
                min_relevance_score=min_score,
                **kwargs
            )
            # Convert RetrievedContext to dicts
            return [self._context_to_dict(r, include_entities, include_relationships) for r in results]
        else:
            # Simple RAG: Use AgentMemory (vector + memory)
            results = self._memory.retrieve(
                query,
                max_results=max_results,
                min_score=min_score,
                **kwargs
            )
            # Convert to dicts
            return [self._memory_to_dict(r) for r in results]

    def forget(
        self,
        memory_id: Optional[str] = None,
        conversation_id: Optional[str] = None,
        user_id: Optional[str] = None,
        days_old: Optional[int] = None,
        **filters
    ) -> int:
        """
        Delete memories.
        
        Args:
            memory_id: Delete specific memory by ID
            conversation_id: Delete all memories in conversation
            user_id: Delete all memories for user
            days_old: Delete memories older than N days
            **filters: Additional filters (type, date_range, etc.)
        
        Returns:
            Number of memories deleted
        
        Example:
            >>> context.forget(memory_id="mem123")
            >>> context.forget(conversation_id="conv1")
            >>> context.forget(days_old=90)
        """
        if memory_id:
            self._memory.delete_memory(memory_id)
            return 1
        
        # Build filters dict
        filter_dict = {}
        if conversation_id:
            filter_dict["conversation_id"] = conversation_id
        if user_id:
            filter_dict["user_id"] = user_id
        if days_old:
            from datetime import datetime, timedelta
            filter_dict["start_date"] = (datetime.now() - timedelta(days=days_old)).isoformat()
        filter_dict.update(filters)
        
        return self._memory.clear_memory(**filter_dict)

    def conversation(
        self,
        conversation_id: str,
        max_items: int = 100,
        reverse: bool = False,
        include_metadata: bool = True,
        **kwargs
    ) -> List[Dict[str, Any]]:
        """
        Get conversation history.
        
        Args:
            conversation_id: Conversation ID
            max_items: Maximum items to return (default: 100)
            reverse: Return in reverse chronological order (default: False)
            include_metadata: Include full metadata (default: True)
            **kwargs: Additional options
        
        Returns:
            List of memory dicts in conversation
        
        Example:
            >>> history = context.conversation("conv1")
            >>> history = context.conversation("conv1", reverse=True, include_metadata=False)
        """
        history = self._memory.get_conversation_history(
            conversation_id=conversation_id,
            max_items=max_items
        )
        
        # Convert to dicts
        result = []
        for item in history:
            item_dict = {
                "id": item.get("id"),
                "content": item.get("content"),
                "timestamp": item.get("timestamp"),
            }
            if include_metadata:
                item_dict["metadata"] = item.get("metadata", {})
            result.append(item_dict)
        
        if reverse:
            result.reverse()
        
        return result

    def _normalize_documents(self, content: List[Union[str, Dict[str, Any]]]) -> List[Dict[str, Any]]:
        """Normalize documents to list of dicts."""
        documents = []
        for i, doc in enumerate(content):
            if isinstance(doc, str):
                documents.append({
                    "content": doc,
                    "id": f"doc_{i}",
                    "metadata": {}
                })
            elif isinstance(doc, dict):
                if "content" not in doc:
                    raise ValueError(f"Document dict must have 'content' key: {doc}")
                
                doc_dict = {
                    "content": doc["content"],
                    "id": doc.get("id", f"doc_{i}"),
                    "metadata": doc.get("metadata", {})
                }
                
                # Preserve entities and relationships if present
                if "entities" in doc:
                    doc_dict["entities"] = doc["entities"]
                if "relationships" in doc:
                    doc_dict["relationships"] = doc["relationships"]
                    
                documents.append(doc_dict)
            else:
                raise ValueError(f"Unsupported document type: {type(doc)}")
        return documents

    def _build_graph_from_documents(
        self,
        documents: List[Dict[str, Any]],
        extract_entities: bool = True,
        extract_relationships: bool = True,
        link_entities: bool = True
    ) -> Dict[str, Any]:
        """Build graph from documents."""
        if not self._graph_builder:
            return {"node_count": 0, "edge_count": 0}
        
        try:
            # Convert documents to conversations format
            conversations = []
            for doc in documents:
                conv = {
                    "id": doc.get("id", "unknown"),
                    "content": doc["content"],
                    "entities": doc.get("entities", []),
                    "relationships": doc.get("relationships", [])
                }
                conversations.append(conv)
            
            # Build graph from conversations
            graph = self._graph_builder.build_from_conversations(
                conversations,
                link_entities=link_entities,
                extract_intents=False,
                extract_sentiments=False
            )
            
            return {
                "node_count": graph.get("statistics", {}).get("node_count", 0),
                "edge_count": graph.get("statistics", {}).get("edge_count", 0),
            }
        except Exception as e:
            self.logger.warning(f"Failed to build graph from documents: {e}")
            return {"node_count": 0, "edge_count": 0}

    def _context_to_dict(
        self,
        context: RetrievedContext,
        include_entities: bool = True,
        include_relationships: bool = False
    ) -> Dict[str, Any]:
        """Convert RetrievedContext to dict."""
        result = {
            "content": context.content,
            "score": context.score,
            "source": context.source,
            "metadata": context.metadata,
        }
        
        if include_entities:
            result["related_entities"] = context.related_entities
        
        if include_relationships:
            result["related_relationships"] = context.related_relationships
        
        return result

    def _memory_to_dict(self, memory: Dict[str, Any]) -> Dict[str, Any]:
        """Convert memory result to dict."""
        return {
            "content": memory.get("content", ""),
            "score": memory.get("score", 0.0),
            "source": "memory",
            "metadata": memory.get("metadata", {}),
            "related_entities": [],
            "related_relationships": [],
        }

    def get_memory(self, memory_id: str) -> Optional[Dict[str, Any]]:
        """
        Get a specific memory by ID.
        
        Args:
            memory_id: Memory ID
        
        Returns:
            Memory dict or None if not found
        
        Example:
            >>> memory = context.get_memory("mem123")
        """
        memory_item = self._memory.get_memory(memory_id)
        if memory_item:
            return {
                "id": memory_item.get("id"),
                "content": memory_item.get("content"),
                "timestamp": memory_item.get("timestamp"),
                "metadata": memory_item.get("metadata", {}),
            }
        return None

    def stats(self) -> Dict[str, Any]:
        """
        Get statistics about stored memories.
        
        Returns:
            Dict with statistics: total_items, items_by_type, etc.
        
        Example:
            >>> stats = context.stats()
            >>> print(f"Total memories: {stats['total_items']}")
        """
        return self._memory.get_statistics()

    def link(
        self,
        text: str,
        entities: Optional[List[Dict[str, Any]]] = None,
        similarity_threshold: float = 0.8,
        **kwargs
    ) -> List[Dict[str, Any]]:
        """
        Link entities in text (if knowledge_graph available).
        
        Args:
            text: Text containing entities
            entities: List of entities to link
            similarity_threshold: Similarity threshold for linking (default: 0.8)
            **kwargs: Additional options
        
        Returns:
            List of linked entity dicts
        
        Raises:
            ValueError: If knowledge_graph not available
        
        Example:
            >>> linked = context.link("Python is used for ML", entities=[...])
        """
        if not self.knowledge_graph:
            raise ValueError(
                "knowledge_graph is required for entity linking. "
                "Please initialize AgentContext with a knowledge_graph parameter. "
                "Example: AgentContext(vector_store=vs, knowledge_graph=kg)"
            )
        
        from .entity_linker import EntityLinker
        
        linker = EntityLinker(
            knowledge_graph=self.knowledge_graph,
            similarity_threshold=similarity_threshold,
            **kwargs
        )
        
        linked_entities = linker.link(text, entities=entities or [])
        
        return [
            {
                "entity_id": e.entity_id,
                "uri": e.uri,
                "text": e.text,
                "type": e.type,
                "linked_count": len(e.linked_entities),
                "confidence": e.confidence,
            }
            for e in linked_entities
        ]

    def build_graph(
        self,
        entities: Optional[List[Dict[str, Any]]] = None,
        relationships: Optional[List[Dict[str, Any]]] = None,
        conversations: Optional[List[Dict[str, Any]]] = None,
        link_entities: bool = True,
        **kwargs
    ) -> Dict[str, Any]:
        """
        Build context graph from entities, relationships, or conversations.
        
        Args:
            entities: List of entities
            relationships: List of relationships
            conversations: List of conversations
            link_entities: Link entities (default: True)
            **kwargs: Additional options
        
        Returns:
            Graph statistics dict
        
        Raises:
            ValueError: If knowledge_graph not available
        
        Example:
            >>> graph = context.build_graph(entities=entities, relationships=relationships)
        """
        if not self._graph_builder:
            raise ValueError(
                "knowledge_graph is required for graph building. "
                "Please initialize AgentContext with a knowledge_graph parameter. "
                "Example: AgentContext(vector_store=vs, knowledge_graph=kg)"
            )
        
        if entities and relationships:
            graph = self._graph_builder.build_from_entities_and_relationships(
                entities, relationships, **kwargs
            )
        elif conversations:
            graph = self._graph_builder.build_from_conversations(
                conversations,
                link_entities=link_entities,
                **kwargs
            )
        else:
            raise ValueError(
                "Must provide either (entities and relationships) or conversations. "
                f"Received: entities={entities is not None}, "
                f"relationships={relationships is not None}, "
                f"conversations={conversations is not None}"
            )
        
        return graph.get("statistics", {})

    # Memory Management Methods
    def exists(self, memory_id: str) -> bool:
        """
        Check if memory exists.
        
        Args:
            memory_id: Memory ID to check
        
        Returns:
            True if memory exists, False otherwise
        
        Example:
            >>> if context.exists("mem123"):
            ...     print("Memory exists")
        """
        return self._memory.get_memory(memory_id) is not None

    def count(self, **filters) -> int:
        """
        Get total memory count with optional filters.
        
        Args:
            **filters: Optional filters (conversation_id, user_id, type, etc.)
        
        Returns:
            Total count of memories matching filters
        
        Example:
            >>> total = context.count()
            >>> conv_count = context.count(conversation_id="conv1")
        """
        stats = self._memory.get_statistics()
        if not filters:
            return stats.get("total_items", 0)
        
        # Filter memories
        count = 0
        for memory_id in self._memory.memory_items:
            memory_item = self._memory.memory_items[memory_id]
            if self._memory._matches_filters(memory_item, filters):
                count += 1
        return count

    def get(self, memory_id: str) -> Optional[Dict[str, Any]]:
        """
        Get memory by ID (alias for get_memory).
        
        Args:
            memory_id: Memory ID
        
        Returns:
            Memory dict or None if not found
        
        Example:
            >>> memory = context.get("mem123")
        """
        return self.get_memory(memory_id)

    def update(
        self,
        memory_id: str,
        content: Optional[str] = None,
        metadata: Optional[Dict[str, Any]] = None,
        **kwargs
    ) -> bool:
        """
        Update existing memory.
        
        Args:
            memory_id: Memory ID to update
            content: New content (optional)
            metadata: New metadata (optional, merged with existing)
            **kwargs: Additional fields to update
        
        Returns:
            True if updated successfully, False if not found
        
        Example:
            >>> context.update("mem123", content="Updated content")
            >>> context.update("mem123", metadata={"new_key": "value"})
        """
        memory_item = self._memory.get_memory(memory_id)
        if not memory_item:
            return False
        
        # Get current values
        current_content = content if content is not None else memory_item.get("content", "")
        current_metadata = memory_item.get("metadata", {})
        if metadata:
            current_metadata.update(metadata)
        
        # Delete old and create new
        self._memory.delete_memory(memory_id)
        new_id = self._memory.store(
            current_content,
            metadata=current_metadata,
            entities=memory_item.get("entities", []),
            relationships=memory_item.get("relationships", []),
            **kwargs
        )
        
        return new_id is not None

    def delete(self, memory_id: str) -> bool:
        """
        Delete memory by ID (alias for forget with memory_id).
        
        Args:
            memory_id: Memory ID to delete
        
        Returns:
            True if deleted, False if not found
        
        Example:
            >>> context.delete("mem123")
        """
        if self.exists(memory_id):
            self._memory.delete_memory(memory_id)
            return True
        return False

    def clear(self, **filters) -> int:
        """
        Clear memories with filters (alias for forget).
        
        Args:
            **filters: Filter criteria (conversation_id, user_id, days_old, etc.)
        
        Returns:
            Number of memories deleted
        
        Example:
            >>> deleted = context.clear(conversation_id="conv1")
            >>> deleted = context.clear(days_old=90)
        """
        return self.forget(**filters)

    def list(
        self,
        conversation_id: Optional[str] = None,
        user_id: Optional[str] = None,
        limit: int = 100,
        offset: int = 0,
        **filters
    ) -> List[Dict[str, Any]]:
        """
        List memories with pagination.
        
        Args:
            conversation_id: Filter by conversation ID
            user_id: Filter by user ID
            limit: Maximum items to return (default: 100)
            offset: Number of items to skip (default: 0)
            **filters: Additional filters
        
        Returns:
            List of memory dicts
        
        Example:
            >>> memories = context.list(conversation_id="conv1", limit=50)
            >>> memories = context.list(user_id="user123", limit=20, offset=10)
        """
        all_filters = {**filters}
        if conversation_id:
            all_filters["conversation_id"] = conversation_id
        if user_id:
            all_filters["user_id"] = user_id
        
        results = []
        for memory_id in list(self._memory.memory_items.keys())[offset:offset + limit]:
            memory_item = self._memory.memory_items[memory_id]
            if not all_filters or self._memory._matches_filters(memory_item, all_filters):
                mem_dict = self._memory.get_memory(memory_id)
                if mem_dict:
                    results.append(mem_dict)
        
        return results

    def batch_store(self, items: List[Union[str, Dict[str, Any]]]) -> List[str]:
        """
        Store multiple items at once.
        
        Args:
            items: List of items to store (strings or dicts with content)
        
        Returns:
            List of memory IDs
        
        Example:
            >>> ids = context.batch_store(["Item 1", "Item 2", "Item 3"])
        """
        memory_ids = []
        for item in items:
            if isinstance(item, str):
                memory_id = self.store(item)
                memory_ids.append(memory_id)
            elif isinstance(item, dict):
                content = item.get("content", "")
                if content:
                    memory_id = self.store(
                        content,
                        metadata=item.get("metadata"),
                        conversation_id=item.get("conversation_id"),
                        user_id=item.get("user_id"),
                        **{k: v for k, v in item.items() if k not in ["content", "metadata", "conversation_id", "user_id"]}
                    )
                    memory_ids.append(memory_id)
        return memory_ids

    def batch_delete(self, memory_ids: List[str]) -> int:
        """
        Delete multiple memories.
        
        Args:
            memory_ids: List of memory IDs to delete
        
        Returns:
            Number of memories deleted
        
        Example:
            >>> deleted = context.batch_delete(["mem1", "mem2", "mem3"])
        """
        deleted = 0
        for memory_id in memory_ids:
            if self.delete(memory_id):
                deleted += 1
        return deleted

    def batch_update(self, updates: List[Dict[str, Any]]) -> int:
        """
        Update multiple memories.
        
        Args:
            updates: List of update dicts with 'memory_id' and fields to update
        
        Returns:
            Number of memories updated
        
        Example:
            >>> updated = context.batch_update([
            ...     {"memory_id": "mem1", "content": "New content"},
            ...     {"memory_id": "mem2", "metadata": {"key": "value"}}
            ... ])
        """
        updated = 0
        for update in updates:
            memory_id = update.get("memory_id")
            if memory_id and self.update(memory_id, **{k: v for k, v in update.items() if k != "memory_id"}):
                updated += 1
        return updated

    # Search and Retrieval Methods
    def search(self, query: str, **filters) -> List[Dict[str, Any]]:
        """
        Simple search with filters (alias for retrieve).
        
        Args:
            query: Search query
            **filters: Additional filters (max_results, min_score, conversation_id, etc.)
        
        Returns:
            List of context dicts
        
        Example:
            >>> results = context.search("Python", max_results=10)
            >>> results = context.search("Python", conversation_id="conv1")
        """
        return self.retrieve(query, **filters)

    def find_similar(self, content: str, limit: int = 5, **kwargs) -> List[Dict[str, Any]]:
        """
        Find similar content.
        
        Args:
            content: Content to find similar items for
            limit: Maximum results (default: 5)
            **kwargs: Additional options
        
        Returns:
            List of similar content dicts
        
        Example:
            >>> similar = context.find_similar("Python programming", limit=5)
        """
        return self.retrieve(content, max_results=limit, **kwargs)

    def get_context(self, query: str, max_results: int = 5, **kwargs) -> List[Dict[str, Any]]:
        """
        Get context for query.
        
        Args:
            query: Query string
            max_results: Maximum results (default: 5)
            **kwargs: Additional options
        
        Returns:
            List of context dicts
        
        Example:
            >>> context_data = context.get_context("Python", max_results=10)
        """
        return self.retrieve(query, max_results=max_results, **kwargs)

    def expand_query(self, query: str, max_hops: int = 2, **kwargs) -> List[Dict[str, Any]]:
        """
        Expand query with graph context.
        
        Args:
            query: Query string
            max_hops: Maximum graph expansion hops (default: 2)
            **kwargs: Additional options
        
        Returns:
            List of expanded context dicts
        
        Example:
            >>> expanded = context.expand_query("Python", max_hops=3)
        """
        if not self._retriever:
            return self.retrieve(query, **kwargs)
        
        return self.retrieve(
            query,
            expand_graph=True,
            max_expansion_hops=max_hops,
            **kwargs
        )

    # Conversation Methods
    def get_conversation(self, conversation_id: str, limit: int = 100, **kwargs) -> List[Dict[str, Any]]:
        """
        Get conversation (alias for conversation).
        
        Args:
            conversation_id: Conversation ID
            limit: Maximum items (default: 100)
            **kwargs: Additional options
        
        Returns:
            List of conversation memory dicts
        
        Example:
            >>> conv = context.get_conversation("conv1", limit=50)
        """
        return self.conversation(conversation_id, max_items=limit, **kwargs)

    def list_conversations(self, user_id: Optional[str] = None, limit: int = 50) -> List[str]:
        """
        List all conversations.
        
        Args:
            user_id: Filter by user ID (optional)
            limit: Maximum conversations to return (default: 50)
        
        Returns:
            List of conversation IDs
        
        Example:
            >>> conversations = context.list_conversations()
            >>> user_convs = context.list_conversations(user_id="user123")
        """
        conversation_ids = set()
        for memory_id, memory_item in self._memory.memory_items.items():
            conv_id = memory_item.metadata.get("conversation_id")
            if conv_id:
                if user_id is None or memory_item.metadata.get("user_id") == user_id:
                    conversation_ids.add(conv_id)
        
        return list(conversation_ids)[:limit]

    def delete_conversation(self, conversation_id: str) -> int:
        """
        Delete entire conversation.
        
        Args:
            conversation_id: Conversation ID to delete
        
        Returns:
            Number of memories deleted
        
        Example:
            >>> deleted = context.delete_conversation("conv1")
        """
        return self.forget(conversation_id=conversation_id)

    def conversation_summary(self, conversation_id: str) -> Dict[str, Any]:
        """
        Get conversation summary.
        
        Args:
            conversation_id: Conversation ID
        
        Returns:
            Summary dict with count, first_message, last_message, etc.
        
        Example:
            >>> summary = context.conversation_summary("conv1")
        """
        history = self.conversation(conversation_id, max_items=1000)
        
        if not history:
            return {
                "conversation_id": conversation_id,
                "message_count": 0,
                "first_message": None,
                "last_message": None,
            }
        
        return {
            "conversation_id": conversation_id,
            "message_count": len(history),
            "first_message": history[0] if history else None,
            "last_message": history[-1] if history else None,
            "user_id": history[0].get("metadata", {}).get("user_id") if history else None,
        }

    # Export/Import Methods
    def export(
        self,
        conversation_id: Optional[str] = None,
        format: str = 'json',
        **filters
    ) -> Union[str, Dict[str, Any]]:
        """
        Export memories.
        
        Args:
            conversation_id: Export specific conversation (optional)
            format: Export format ('json' or 'dict', default: 'json')
            **filters: Additional filters
        
        Returns:
            Exported data (JSON string or dict)
        
        Example:
            >>> data = context.export(conversation_id="conv1")
            >>> data = context.export(format='dict')
        """
        all_filters = {**filters}
        if conversation_id:
            all_filters["conversation_id"] = conversation_id
        
        memories = self.list(**all_filters)
        
        export_data = {
            "exported_at": datetime.now().isoformat(),
            "count": len(memories),
            "memories": memories
        }
        
        if format == 'json':
            import json
            return json.dumps(export_data, indent=2, default=str)
        return export_data

    def import_data(self, data: Union[str, Dict[str, Any]], format: str = 'json') -> int:
        """
        Import memories.
        
        Args:
            data: Data to import (JSON string or dict)
            format: Data format ('json' or 'dict', default: 'json')
        
        Returns:
            Number of memories imported
        
        Example:
            >>> imported = context.import_data(json_string)
            >>> imported = context.import_data(data_dict, format='dict')
        """
        if format == 'json':
            import json
            if isinstance(data, str):
                data = json.loads(data)
        
        if not isinstance(data, dict):
            raise ValueError("Invalid data format. Expected dict or JSON string.")
        
        memories = data.get("memories", [])
        if not memories:
            return 0
        
        imported = 0
        for memory in memories:
            try:
                memory_id = self.store(
                    memory.get("content", ""),
                    metadata=memory.get("metadata", {}),
                    conversation_id=memory.get("metadata", {}).get("conversation_id"),
                    user_id=memory.get("metadata", {}).get("user_id"),
                )
                if memory_id:
                    imported += 1
            except Exception as e:
                self.logger.warning(f"Failed to import memory: {e}")
        
        return imported

    def backup(self, **filters) -> str:
        """
        Create backup (alias for export).
        
        Args:
            **filters: Filter criteria
        
        Returns:
            JSON string of backup data
        
        Example:
            >>> backup_data = context.backup()
            >>> backup_data = context.backup(conversation_id="conv1")
        """
        return self.export(format='json', **filters)

    def restore(self, data: Union[str, Dict[str, Any]], format: str = 'json') -> int:
        """
        Restore from backup (alias for import_data).
        
        Args:
            data: Backup data (JSON string or dict)
            format: Data format ('json' or 'dict', default: 'json')
        
        Returns:
            Number of memories restored
        
        Example:
            >>> restored = context.restore(backup_data)
        """
        return self.import_data(data, format=format)

    # Statistics and Analytics
    def health(self) -> Dict[str, Any]:
        """
        Check system health.
        
        Returns:
            Health status dict
        
        Example:
            >>> health = context.health()
            >>> print(f"Status: {health['status']}")
        """
        stats = self.stats()
        total = stats.get("total_items", 0)
        
        health_status = {
            "status": "healthy" if total >= 0 else "error",
            "total_memories": total,
            "vector_store_available": self.vector_store is not None,
            "knowledge_graph_available": self.knowledge_graph is not None,
            "retriever_available": self._retriever is not None,
            "graph_builder_available": self._graph_builder is not None,
        }
        
        return health_status

    def usage_stats(self, period: str = 'day') -> Dict[str, Any]:
        """
        Get usage statistics.
        
        Args:
            period: Time period ('day', 'week', 'month', default: 'day')
        
        Returns:
            Usage statistics dict
        
        Example:
            >>> usage = context.usage_stats(period='week')
        """
        from datetime import datetime, timedelta
        
        now = datetime.now()
        if period == 'day':
            start = now - timedelta(days=1)
        elif period == 'week':
            start = now - timedelta(weeks=1)
        elif period == 'month':
            start = now - timedelta(days=30)
        else:
            start = now - timedelta(days=1)
        
        stats = self.stats()
        total = stats.get("total_items", 0)
        
        # Count recent memories
        recent_count = 0
        for memory_id, memory_item in self._memory.memory_items.items():
            if memory_item.timestamp >= start:
                recent_count += 1
        
        return {
            "period": period,
            "total_memories": total,
            "recent_memories": recent_count,
            "conversations": len(self.list_conversations()),
        }

