import os
import glob
from typing import List, Dict, Any, Optional
from datetime import datetime

import aiofiles
import chromadb
from chromadb.config import Settings
from sentence_transformers import SentenceTransformer
from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver

# Prevent ChromaDB from reporting telemetry to the mothership
os.environ["ANONYMIZED_TELEMETRY"] = "False"

class MemoryManager:
    """Manages persistent memory using local tools:

    - LangGraph checkpointing (SQLite) for conversation state
    - ChromaDB for semantic memory search
    - Local sentence-transformers for embeddings
    - Memory graph approach for clustering related concepts
    """

    def __init__(
        self,
        checkpoint_db_path: str = "data/checkpoints.db",
        chroma_db_path: str = "data/chroma",
        embedding_model: str = "all-MiniLM-L6-v2",
        context_docs_dir: str = "memory",
        base_dir: str = "."
    ):
        """Initialize memory manager.

        Args:
            checkpoint_db_path: Path to SQLite checkpoint database
            chroma_db_path: Path to ChromaDB directory
            embedding_model: Sentence-transformers model name
            context_docs_dir: Directory containing markdown context files
            base_dir: Base directory for resolving relative paths
        """
        self.checkpoint_db_path = checkpoint_db_path
        self.chroma_db_path = chroma_db_path
        self.embedding_model_name = embedding_model
        self.context_docs_dir = os.path.join(base_dir, context_docs_dir)

        # Will be initialized on startup
        self.checkpointer: Optional[AsyncSqliteSaver] = None
        self.checkpointer_context: Optional[Any] = None  # Store the context manager
        self.chroma_client: Optional[chromadb.Client] = None
        self.memory_collection: Optional[Any] = None
        self.embedding_model: Optional[SentenceTransformer] = None

        self.context_documents: Dict[str, str] = {}
        self.initialized = False

    async def initialize(self) -> None:
        """Initialize the memory system and load context documents."""
        if self.initialized:
            return

        # Ensure data directories exist
        os.makedirs(os.path.dirname(self.checkpoint_db_path), exist_ok=True)
        os.makedirs(self.chroma_db_path, exist_ok=True)

        # Initialize LangGraph checkpointer (SQLite)
        self.checkpointer_context = AsyncSqliteSaver.from_conn_string(
            self.checkpoint_db_path
        )
        self.checkpointer = await self.checkpointer_context.__aenter__()
        await self.checkpointer.setup()

        # Initialize ChromaDB
        self.chroma_client = chromadb.PersistentClient(
            path=self.chroma_db_path,
            settings=Settings(
                anonymized_telemetry=False,
                allow_reset=True
            )
        )

        # Get or create memory collection
        self.memory_collection = self.chroma_client.get_or_create_collection(
            name="conversation_memory",
            metadata={"description": "Semantic memory for conversations"}
        )

        # Initialize local embedding model
        print(f"Loading embedding model: {self.embedding_model_name}")
        self.embedding_model = SentenceTransformer(self.embedding_model_name)

        # Load markdown context documents
        await self._load_context_documents()

        # Index context documents in ChromaDB
        await self._index_context_documents()

        self.initialized = True
        print("Memory system initialized (LangGraph + ChromaDB + local embeddings)")

    async def _load_context_documents(self) -> None:
        """Load all markdown files from context directory."""
        if not os.path.exists(self.context_docs_dir):
            print(f"Warning: Context directory {self.context_docs_dir} not found")
            return

        md_files = glob.glob(os.path.join(self.context_docs_dir, "*.md"))

        for md_file in md_files:
            try:
                async with aiofiles.open(md_file, "r", encoding="utf-8") as f:
                    content = await f.read()
                    filename = os.path.basename(md_file)
                    self.context_documents[filename] = content
                    print(f"Loaded context document: {filename}")
            except Exception as e:
                print(f"Error loading {md_file}: {e}")

    async def _index_context_documents(self) -> None:
        """Index context documents in ChromaDB for semantic search."""
        if not self.context_documents or not self.memory_collection:
            return

        for filename, content in self.context_documents.items():
            # Split into sections (by headers)
            sections = self._split_document_into_sections(content, filename)

            for i, section in enumerate(sections):
                doc_id = f"context_{filename}_{i}"

                # Generate embedding
                embedding = self.embedding_model.encode(section["content"]).tolist()

                # Add to ChromaDB
                self.memory_collection.add(
                    ids=[doc_id],
                    embeddings=[embedding],
                    documents=[section["content"]],
                    metadatas=[{
                        "type": "context",
                        "source": filename,
                        "section": section["title"],
                        "indexed_at": datetime.utcnow().isoformat()
                    }]
                )

        print(f"Indexed {len(self.context_documents)} context documents")

    def _split_document_into_sections(self, content: str, filename: str) -> List[Dict[str, str]]:
        """Split markdown document into logical sections.

        Args:
            content: Markdown content
            filename: Source filename

        Returns:
            List of section dicts with title and content
        """
        sections = []
        current_section = {"title": filename, "content": ""}

        for line in content.split("\n"):
            if line.startswith("#"):
                # New section
                if current_section["content"].strip():
                    sections.append(current_section)
                current_section = {
                    "title": line.strip("#").strip(),
                    "content": line + "\n"
                }
            else:
                current_section["content"] += line + "\n"

        # Add last section
        if current_section["content"].strip():
            sections.append(current_section)

        return sections

    def get_context_prompt(self) -> str:
        """Generate a context prompt from loaded documents.

        system_prompt.md is ALWAYS included first and prioritized.
        Other documents are included after.

        Returns:
            Formatted string containing all context documents
        """
        if not self.context_documents:
            return ""

        sections = []

        # ALWAYS include system_prompt.md first if it exists
        system_prompt_key = "system_prompt.md"
        if system_prompt_key in self.context_documents:
            sections.append(self.context_documents[system_prompt_key])
            sections.append("\n---\n")

        # Add other context documents
        sections.append("# Additional Context\n")
        sections.append("The following documents provide additional context about the system:\n")

        for filename, content in sorted(self.context_documents.items()):
            # Skip system_prompt.md since we already added it
            if filename == system_prompt_key:
                continue

            sections.append(f"\n## {filename}\n")
            sections.append(content)

        return "\n".join(sections)

    async def add_memory(
        self,
        session_id: str,
        role: str,
        content: str,
        metadata: Optional[Dict[str, Any]] = None
    ) -> None:
        """Add a message to semantic memory (ChromaDB).

        Args:
            session_id: Session identifier
            role: Message role ("user" or "assistant")
            content: Message content
            metadata: Optional metadata
        """
        if not self.memory_collection or not self.embedding_model:
            return

        try:
            # Generate unique ID
            timestamp = datetime.utcnow().isoformat()
            doc_id = f"{session_id}_{role}_{timestamp}"

            # Generate embedding
            embedding = self.embedding_model.encode(content).tolist()

            # Prepare metadata
            meta = {
                "session_id": session_id,
                "role": role,
                "timestamp": timestamp,
                "type": "conversation",
                **(metadata or {})
            }

            # Add to ChromaDB
            self.memory_collection.add(
                ids=[doc_id],
                embeddings=[embedding],
                documents=[content],
                metadatas=[meta]
            )
        except Exception as e:
            print(f"Error adding to ChromaDB memory: {e}")

    async def search_memory(
        self,
        session_id: str,
        query: str,
        limit: int = 5,
        include_context: bool = True
    ) -> List[Dict[str, Any]]:
        """Search memory using semantic similarity.

        Args:
            session_id: Session identifier (filters to this session + context docs)
            query: Search query
            limit: Maximum results
            include_context: Whether to include context documents in search

        Returns:
            List of relevant memory items with content and metadata
        """
        if not self.memory_collection or not self.embedding_model:
            return []

        try:
            # Generate query embedding
            query_embedding = self.embedding_model.encode(query).tolist()

            # Build where filter
            where_filters = []
            if include_context:
                # Search both session messages and context docs
                where_filters = {
                    "$or": [
                        {"session_id": session_id},
                        {"type": "context"}
                    ]
                }
            else:
                where_filters = {"session_id": session_id}

            # Query ChromaDB
            results = self.memory_collection.query(
                query_embeddings=[query_embedding],
                n_results=limit,
                where=where_filters if where_filters else None
            )

            # Format results
            memories = []
            if results and results["documents"]:
                for i, doc in enumerate(results["documents"][0]):
                    memories.append({
                        "content": doc,
                        "metadata": results["metadatas"][0][i],
                        "distance": results["distances"][0][i] if "distances" in results else None
                    })

            return memories
        except Exception as e:
            print(f"Error searching ChromaDB memory: {e}")
            return []

    async def get_memory_graph(
        self,
        session_id: str,
        max_depth: int = 2
    ) -> Dict[str, Any]:
        """Get a graph of related memories using clustering.

        This creates a simple memory graph by finding clusters of related concepts.

        Args:
            session_id: Session identifier
            max_depth: Maximum depth for related memory traversal

        Returns:
            Dict representing memory graph structure
        """
        # Simple implementation: get all memories for session and cluster by similarity
        if not self.memory_collection:
            return {}

        try:
            # Get all memories for this session
            results = self.memory_collection.get(
                where={"session_id": session_id},
                include=["embeddings", "documents", "metadatas"]
            )

            if not results or not results["documents"]:
                return {"nodes": [], "edges": []}

            # Build simple graph structure
            nodes = []
            edges = []

            for i, doc in enumerate(results["documents"]):
                nodes.append({
                    "id": results["ids"][i],
                    "content": doc,
                    "metadata": results["metadatas"][i]
                })

            # TODO: Compute edges based on embedding similarity
            # For now, return just nodes
            return {"nodes": nodes, "edges": edges}

        except Exception as e:
            print(f"Error building memory graph: {e}")
            return {}

    def get_checkpointer(self) -> Optional[AsyncSqliteSaver]:
        """Get the LangGraph checkpointer for conversation state.

        Returns:
            AsyncSqliteSaver instance for LangGraph persistence
        """
        return self.checkpointer

    async def close(self) -> None:
        """Close the memory manager and cleanup resources."""
        if self.checkpointer_context:
            await self.checkpointer_context.__aexit__(None, None, None)
            self.checkpointer = None
            self.checkpointer_context = None
