"""Semantic Memory Store — NetworkX knowledge graph + SQLite persistence.

Stores facts, relationships, and knowledge as a directed graph.
De-duplicated, verified, entity-based lookup.
Maps to the neocortex in human cognitive architecture.
"""

from __future__ import annotations

import json
import sqlite3
from datetime import datetime, timezone
from typing import Optional

import networkx as nx

from mnemosynth.core.types import MemoryNode, MemoryType, MemoryStatus, RelationshipEdge
from mnemosynth.stores.base import BaseStore


class SemanticStore(BaseStore):
    """NetworkX + SQLite knowledge graph for semantic memories."""

    def __init__(self, config, db_manager):
        self.config = config
        self.db = db_manager
        self.graph = nx.DiGraph()
        self._load_graph()

    def _load_graph(self) -> None:
        """Load the knowledge graph from SQLite."""
        # Load nodes (semantic memories from DB)
        memories = self.db.get_memories(memory_type=MemoryType.SEMANTIC, limit=10000)
        for mem in memories:
            self.graph.add_node(
                mem.id,
                content=mem.content,
                confidence=mem.confidence,
                created_at=mem.created_at.isoformat(),
                status=mem.status.value,
                corroboration_count=mem.corroboration_count,
            )

        # Load edges (relationships from DB)
        for mem in memories:
            edges = self.db.get_relationships(mem.id, direction="outgoing")
            for edge in edges:
                self.graph.add_edge(
                    edge.source_id,
                    edge.target_id,
                    relation_type=edge.relation_type,
                    confidence=edge.confidence,
                    created_at=edge.created_at.isoformat(),
                )

    def add(self, memory: MemoryNode) -> None:
        """Add a semantic fact to the knowledge graph."""
        # Check for duplicates
        existing = self._find_similar(memory.content)
        if existing:
            # Corroborate existing fact instead of duplicating
            existing.corroborate()
            self.db.save_memory(existing)
            self.graph.nodes[existing.id]["confidence"] = existing.confidence
            self.graph.nodes[existing.id]["corroboration_count"] = existing.corroboration_count
            return

        # Add new node to graph
        self.graph.add_node(
            memory.id,
            content=memory.content,
            confidence=memory.confidence,
            created_at=memory.created_at.isoformat(),
            status=memory.status.value,
            corroboration_count=memory.corroboration_count,
        )

    def add_relationship(self, edge: RelationshipEdge) -> None:
        """Add a relationship edge between two memory nodes."""
        self.graph.add_edge(
            edge.source_id,
            edge.target_id,
            relation_type=edge.relation_type,
            confidence=edge.confidence,
            created_at=edge.created_at.isoformat(),
        )
        self.db.save_relationship(edge)

    def search(self, query: str, limit: int = 5) -> list[MemoryNode]:
        """Search semantic memories by keyword matching on graph nodes."""
        query_lower = query.lower()
        scored: list[tuple[float, MemoryNode]] = []

        for node_id, data in self.graph.nodes(data=True):
            content = data.get("content", "")
            if not content:
                continue

            # Simple relevance scoring based on keyword overlap
            content_lower = content.lower()
            query_words = query_lower.split()
            matches = sum(1 for w in query_words if w in content_lower)

            if matches > 0:
                score = (matches / len(query_words)) * data.get("confidence", 0.85)
                mem = self._node_to_memory(node_id, data)
                if mem.status == MemoryStatus.ACTIVE:
                    scored.append((score, mem))

        # Sort by score descending
        scored.sort(key=lambda x: x[0], reverse=True)
        return [m for _, m in scored[:limit]]

    def get(self, memory_id: str) -> Optional[MemoryNode]:
        """Get a specific semantic memory by ID."""
        if memory_id not in self.graph:
            return None
        data = self.graph.nodes[memory_id]
        return self._node_to_memory(memory_id, data)

    def update(self, memory: MemoryNode) -> None:
        """Update a semantic memory node."""
        if memory.id in self.graph:
            self.graph.nodes[memory.id].update({
                "content": memory.content,
                "confidence": memory.confidence,
                "status": memory.status.value,
                "corroboration_count": memory.corroboration_count,
            })

    def delete(self, memory_id: str) -> bool:
        """Remove a node and its edges from the graph."""
        if memory_id in self.graph:
            self.graph.remove_node(memory_id)
            return True
        return False

    def deprecate_fact(self, old_id: str, new_id: str) -> None:
        """Deprecate an old fact and link to its replacement."""
        if old_id in self.graph:
            self.graph.nodes[old_id]["status"] = MemoryStatus.DEPRECATED.value

        # Add overwritten_by edge
        edge = RelationshipEdge(
            source_id=old_id,
            target_id=new_id,
            relation_type="overwritten_by",
            confidence=0.95,
        )
        self.add_relationship(edge)

    def get_related(self, memory_id: str, depth: int = 1) -> list[MemoryNode]:
        """Get related nodes within N hops."""
        if memory_id not in self.graph:
            return []

        related_ids = set()
        current_level = {memory_id}

        for _ in range(depth):
            next_level = set()
            for nid in current_level:
                # Outgoing neighbors
                next_level.update(self.graph.successors(nid))
                # Incoming neighbors
                next_level.update(self.graph.predecessors(nid))
            related_ids.update(next_level)
            current_level = next_level - related_ids  # Only new ones

        related_ids.discard(memory_id)  # Remove the query node itself

        results = []
        for nid in related_ids:
            data = self.graph.nodes.get(nid, {})
            if data:
                results.append(self._node_to_memory(nid, data))

        return results

    def get_entities(self) -> list[dict]:
        """Get all entities (nodes) in the knowledge graph."""
        entities = []
        for node_id, data in self.graph.nodes(data=True):
            entities.append({
                "id": node_id,
                "content": data.get("content", ""),
                "confidence": data.get("confidence", 0.85),
                "connections": self.graph.degree(node_id),
            })
        return entities

    def count(self) -> int:
        """Count total nodes in the knowledge graph."""
        return self.graph.number_of_nodes()

    def _find_similar(self, content: str, threshold: float = 0.9) -> Optional[MemoryNode]:
        """Check if a very similar fact already exists (simple word overlap)."""
        content_words = set(content.lower().split())
        if not content_words:
            return None

        for node_id, data in self.graph.nodes(data=True):
            existing_words = set(data.get("content", "").lower().split())
            if not existing_words:
                continue

            overlap = len(content_words & existing_words) / max(
                len(content_words), len(existing_words)
            )
            if overlap >= threshold:
                return self._node_to_memory(node_id, data)

        return None

    def _node_to_memory(self, node_id: str, data: dict) -> MemoryNode:
        """Convert a graph node to a MemoryNode."""
        return MemoryNode(
            id=node_id,
            content=data.get("content", ""),
            memory_type=MemoryType.SEMANTIC,
            confidence=data.get("confidence", 0.85),
            created_at=datetime.fromisoformat(
                data.get("created_at", datetime.now(timezone.utc).isoformat())
            ),
            last_accessed=datetime.now(timezone.utc),
            status=MemoryStatus(data.get("status", "active")),
            corroboration_count=data.get("corroboration_count", 0),
        )
