#!/usr/bin/env python3
"""
Genesis Memory Cortex
=====================
World-Leading Multi-Tier Memory System with MCP Integration

Unifies all memory systems into a single intelligent architecture:
- Working Memory (Dragonfly) - Fast session context with vector search
- Episodic Memory (SQLite) - Experiential storage with FTS
- Semantic Memory (Neo4j/MCP) - Conceptual knowledge graphs
- Vector Memory (Weaviate/ChromaDB/Dragonfly) - Multi-backend similarity search

Usage:
    from genesis_memory_cortex import MemoryCortex

    cortex = MemoryCortex()
    cortex.remember("Discovered that parallel execution improves performance by 37%")
    results = cortex.recall("performance optimization")

Vector Backends:
    - Redis (Remote/Shared, HNSW indexing) - Fast working memory
    - Qdrant (Remote, episodic vector store) - Experiential storage
    - SQLite (Local, tempo-spatial events) - Episodic events
    - Weaviate/ChromaDB (Optional multi-backend similarity)
"""

import json
import hashlib
import hashlib
# sqlite3 removed
import redis
import os
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional, Tuple
from dataclasses import dataclass, asdict
from pathlib import Path
from enum import Enum
import threading
import time

# Import existing Genesis memory components
from core.surprise_memory import MemorySystem, MemoryItem, SurpriseScore

# Import observability modules
try:
    from logging_config import get_logger, with_context, OperationTimer
    LOGGING_AVAILABLE = True
    logger = get_logger("genesis.cortex")
except ImportError:
    LOGGING_AVAILABLE = False
    logger = None

try:
    from metrics import GenesisMetrics, TimedOperation
    METRICS_AVAILABLE = True
except ImportError:
    METRICS_AVAILABLE = False
    GenesisMetrics = None

# Import circuit breaker for resilience
try:
    from circuit_breaker import get_circuit_breaker, CircuitBreaker
    CIRCUIT_AVAILABLE = True
except ImportError:
    CIRCUIT_AVAILABLE = False
    get_circuit_breaker = None

# Import secrets loader for secure credential management
try:
    from secrets_loader import get_redis_config, RedisConfig
    SECRETS_AVAILABLE = True
except ImportError:
    SECRETS_AVAILABLE = False
    get_redis_config = None
    RedisConfig = None

# Import vector backends for semantic similarity search
try:
    from vector_backends import VectorManager, VectorDocument
    VECTOR_AVAILABLE = True
except ImportError:
    VECTOR_AVAILABLE = False
    VectorManager = None


class MemoryTier(Enum):
    """Memory storage tiers based on importance and access patterns."""
    DISCARD = "discard"      # Score < 0.3 - not stored
    WORKING = "working"       # Score 0.3-0.5 - Redis/session cache
    EPISODIC = "episodic"     # Score 0.5-0.8 - PostgreSQL (Elestio)
    SEMANTIC = "semantic"     # Score >= 0.8 - Knowledge Graph (PostgreSQL/MCP)


@dataclass
class Memory:
    """Unified memory representation."""
    id: str
    content: str
    tier: MemoryTier
    score: float
    domain: str
    source: str
    timestamp: str
    embedding: Optional[List[float]] = None
    relations: Optional[List[str]] = None
    access_count: int = 0
    last_accessed: Optional[str] = None
    metadata: Optional[Dict] = None

    def to_dict(self) -> Dict:
        d = asdict(self)
        d['tier'] = self.tier.value
        return d


class WorkingMemoryCache:
    """
    Redis-backed cache for working memory tier.
    Provides real-time continuum across all Genesis agents.

    Credentials loaded from environment or secrets.env file.
    See: secrets_loader.py for configuration.

    Features:
    - Circuit breaker for resilience
    - Structured logging
    - Metrics collection
    - Adaptive TTL
    """

    def __init__(self, redis_config: Optional['RedisConfig'] = None,
                 ttl_seconds: int = 3600):
        self.namespace = "genesis:working_memory"
        self.ttl = ttl_seconds
        self.client = None
        self.available = False

        # Circuit breaker for Redis operations
        self._circuit = None
        if CIRCUIT_AVAILABLE and get_circuit_breaker:
            self._circuit = get_circuit_breaker(
                "working_memory_redis",
                failure_threshold=5,
                recovery_timeout=30.0
            )

        # Load config from secrets if not provided
        if redis_config is None:
            if SECRETS_AVAILABLE and get_redis_config:
                redis_config = get_redis_config()
            else:
                if logger:
                    logger.warning("No secrets_loader available, using fallback")
                else:
                    print("[!] WorkingMemoryCache: No secrets_loader available, using fallback")
                self.available = False
                return

        # Check if Redis is configured
        if not redis_config.is_configured:
            if logger:
                logger.info("Redis not configured, operating without cache")
            else:
                print("[!] WorkingMemoryCache: Redis not configured, operating without cache")
            self.available = False
            return

        try:
            self.client = redis.Redis(
                host=redis_config.host,
                port=redis_config.port,
                password=redis_config.password,
                ssl=redis_config.ssl,
                socket_timeout=5,
                decode_responses=True
            )
            # Verify connectivity
            self.client.ping()
            self.available = True
            if logger:
                logger.info("Connected to Redis", extra={"host": redis_config.host})
            else:
                print(f"[OK] WorkingMemoryCache: Connected to Redis at {redis_config.host}")
        except Exception as e:
            if logger:
                logger.error("Redis connection failed", extra={"error": str(e)})
            else:
                print(f"[!] WorkingMemoryCache Redis Error: {e}")
                print("[!] Operating in degraded mode without working memory cache")
            self.available = False
            self.client = None

    def set(self, memory: Memory, adaptive_ttl: bool = True):
        """Store memory in Redis with adaptive TTL based on importance."""
        # Check circuit breaker
        if self._circuit and not self._circuit.is_available:
            if logger:
                logger.warning("Circuit breaker open, skipping Redis write")
            return

        memory_data = json.dumps(memory.to_dict())

        # Calculate adaptive TTL
        ttl = self._calculate_ttl(memory) if adaptive_ttl else self.ttl

        start_time = time.time()
        try:
            if self.client:
                self.client.setex(
                    f"{self.namespace}:{memory.id}",
                    ttl,
                    memory_data
                )
                # Record success
                if self._circuit:
                    self._circuit.record_success()
                if METRICS_AVAILABLE and GenesisMetrics:
                    duration = time.time() - start_time
                    GenesisMetrics.memory_operations.inc(labels={"tier": "working", "op": "store"})
                    GenesisMetrics.memory_latency.observe(duration, labels={"tier": "working"})
                if logger:
                    logger.debug("Memory stored", extra={"id": memory.id, "ttl": ttl})
        except Exception as e:
            if self._circuit:
                self._circuit.record_failure(e)
            if logger:
                logger.error("Redis write failed", extra={"error": str(e), "id": memory.id})
            else:
                print(f"[!] Redis write failed: {e}")
            # Graceful degradation instead of halt

    def _calculate_ttl(self, memory: Memory) -> int:
        """
        Calculate adaptive TTL based on memory importance.

        Factors:
        - Base TTL (1 hour)
        - Score bonus: High score = longer retention
        - Domain bonus: Certain domains get longer retention
        - Length bonus: More content = likely more important

        Returns TTL in seconds.
        """
        base_ttl = self.ttl  # Default 3600 seconds

        # Score multiplier (0.5x to 2x based on score)
        score = memory.score if hasattr(memory, 'score') else 0.5
        score_multiplier = 0.5 + (score * 1.5)  # Range: 0.5 - 2.0

        # Domain multiplier
        important_domains = {"security", "critical", "insight", "learning", "axiom"}
        domain = memory.domain if hasattr(memory, 'domain') else ""
        domain_multiplier = 1.5 if domain.lower() in important_domains else 1.0

        # Content length bonus (log scale)
        content = memory.content if hasattr(memory, 'content') else ""
        length_bonus = min(len(content) / 1000, 0.5)  # Max 0.5x bonus

        # Calculate final TTL
        ttl = int(base_ttl * score_multiplier * domain_multiplier * (1 + length_bonus))

        # Clamp to reasonable range (5 min to 24 hours)
        return max(300, min(ttl, 86400))

    def get(self, memory_id: str, extend_ttl: bool = True) -> Optional[Memory]:
        """Retrieve memory from Redis, optionally extending TTL on access."""
        # Check circuit breaker
        if self._circuit and not self._circuit.is_available:
            if METRICS_AVAILABLE and GenesisMetrics:
                GenesisMetrics.cache_misses.inc(labels={"tier": "working"})
            return None

        if self.available and self.client:
            start_time = time.time()
            try:
                key = f"{self.namespace}:{memory_id}"
                data = self.client.get(key)
                if data:
                    mem_dict = json.loads(data)
                    memory = self._dict_to_memory(mem_dict)

                    # Track access count
                    access_key = f"{self.namespace}:access_counts"
                    access_count = self.client.hincrby(access_key, memory_id, 1)

                    # Extend TTL on access (up to 2x original based on access count)
                    if extend_ttl:
                        current_ttl = self.client.ttl(key)
                        if current_ttl > 0:
                            # Each access adds 10% more time (up to 2x)
                            extension = min(access_count * 0.1, 1.0)
                            new_ttl = int(current_ttl * (1 + extension * 0.5))
                            new_ttl = min(new_ttl, 86400)  # Cap at 24 hours
                            self.client.expire(key, new_ttl)

                    # Record metrics
                    if self._circuit:
                        self._circuit.record_success()
                    if METRICS_AVAILABLE and GenesisMetrics:
                        duration = time.time() - start_time
                        GenesisMetrics.cache_hits.inc(labels={"tier": "working"})
                        GenesisMetrics.memory_operations.inc(labels={"tier": "working", "op": "get"})
                        GenesisMetrics.memory_latency.observe(duration, labels={"tier": "working"})

                    return memory
                else:
                    if METRICS_AVAILABLE and GenesisMetrics:
                        GenesisMetrics.cache_misses.inc(labels={"tier": "working"})
            except Exception as e:
                if self._circuit:
                    self._circuit.record_failure(e)
                if logger:
                    logger.error("Redis get failed", extra={"error": str(e), "id": memory_id})
                else:
                    print(f"[!] Redis get failed: {e}")
        return None

    def search(self, query: str, limit: int = 5) -> List[Memory]:
        """Search working memory keys."""
        results = []
        if self.available:
            try:
                # This is a basic scan, in production we'd use Redis Search (RediSearch)
                for key in self.client.scan_iter(f"{self.namespace}:*"):
                    data = self.client.get(key)
                    if data:
                        mem_dict = json.loads(data)
                        if query.lower() in mem_dict['content'].lower():
                            results.append(self._dict_to_memory(mem_dict))
                    if len(results) >= limit:
                        break
            except:
                pass
        return results

    def _dict_to_memory(self, d: Dict) -> Memory:
        """Helper to restore Memory object from dict."""
        from genesis_memory_cortex import MemoryTier
        d['tier'] = MemoryTier(d['tier'])
        return Memory(**d)

    def stats(self) -> Dict:
        """Get cache statistics."""
        if self.available:
            try:
                count = len(list(self.client.scan_iter(f"{self.namespace}:*")))
                return {
                    "backend": "redis",
                    "size": count,
                    "ttl_seconds": self.ttl,
                    "available": True
                }
            except:
                pass
        return {"backend": "fallback", "available": False}

    def get_promotable(self, access_threshold: int = 3) -> List[Memory]:
        """
        Get memories ready for promotion to episodic tier.
        In working memory, we promote based on access count tracked via Redis.
        """
        results = []
        if self.available:
            try:
                access_key = f"{self.namespace}:access_counts"
                for key in self.client.scan_iter(f"{self.namespace}:*"):
                    if ":access_counts" in key:
                        continue
                    memory_id = key.split(":")[-1]
                    access_count = int(self.client.hget(access_key, memory_id) or 0)
                    if access_count >= access_threshold:
                        data = self.client.get(key)
                        if data:
                            results.append(self._dict_to_memory(json.loads(data)))
            except Exception:
                pass
        return results

    def remove(self, memory_id: str) -> bool:
        """Remove a memory from working cache."""
        if self.available:
            try:
                self.client.delete(f"{self.namespace}:{memory_id}")
                self.client.hdel(f"{self.namespace}:access_counts", memory_id)
                return True
            except:
                pass
        return False


class EpisodicMemoryStore:
    """
    SQLite-based episodic memory storage.
    Stores experiential memories with temporal context.
    """

    def __init__(self, db_path: str = None, use_postgres: bool = True):
        self.use_postgres = use_postgres
        self.pg_store = None
        
        # STRICT MODE: PostgreSQL Only (Elestio)
        if use_postgres:
            try:
                # Import from data directory
                import sys
                sys.path.append(r"E:\genesis-system\data\genesis-memory")
                from storage.postgresql_store import PostgreSQLStore
                from elestio_config import PostgresConfig
                
                self.pg_store = PostgreSQLStore(**PostgresConfig.get_connection_params())
                print("[OK] EpisodicMemoryStore: PostgreSQL (Elestio) backend active")
            except Exception as e:
                # CRITICAL: Do not fall back
                print(f"[!] CRITICAL FAILURE: PostgreSQL Connection Failed: {e}")
                print("[!] Fall back to SQLite is FORBIDDEN by user directive.")
                raise ConnectionError(f"PostgreSQL connection failed. Strict mode enabled. Error: {e}")
        else:
            raise ValueError("EpisodicMemoryStore must use PostgreSQL in production.")

    def _init_db(self):
        """Initialize database (PostgreSQL Strict Mode)."""
        if not self.use_postgres:
             raise ValueError("SQLite fallback is disabled in Strict Mode.")
        # PostgreSQL initialized in __init__

    def store(self, memory: Memory) -> str:
        """Store episodic memory (PostgreSQL Strict)."""
        if not self.pg_store:
             raise ConnectionError("PostgreSQL Store not initialized (Strict Mode)")

        return self.pg_store.store_episode(
            content=memory.content,
            source_type=memory.source,
            agent_id=memory.metadata.get("agent_id") if memory.metadata else None,
            importance_score=memory.score,
            surprise_score=memory.metadata.get("surprise_score", 0.0) if memory.metadata else 0.0
        ) or memory.id

    def search(self, query: str, limit: int = 10) -> List[Memory]:
        """Search for memories (PostgreSQL Strict)."""
        if not self.pg_store:
             raise ConnectionError("PostgreSQL Store not initialized (Strict Mode)")
             
        episodes = self.pg_store.search_episodes(
            query=query,
            limit=limit,
            status='active'
        )
        return [self._episode_to_memory(e) for e in episodes]

    def get(self, memory_id: str) -> Optional[Memory]:
        """Get specific memory by ID."""
        if not self.pg_store:
             raise ConnectionError("PostgreSQL Store not initialized (Strict Mode)")
             
        ep = self.pg_store.get_episode(memory_id)
        if ep:
            return self._episode_to_memory(ep)
        return None

    def get_by_domain(self, domain: str, limit: int = 20) -> List[Memory]:
        """Get memories by domain."""
        if not self.pg_store:
             raise ConnectionError("PostgreSQL Store not initialized (Strict Mode)")
             
        # Map source_type to domain for compatibility
        episodes = self.pg_store.search_episodes(
            source_type=domain,
            limit=limit
        )
        return [self._episode_to_memory(e) for e in episodes]

    def get_promotable(self, score_threshold: float = 0.75,
                       access_threshold: int = 5) -> List[Memory]:
        """Get memories ready for promotion."""
        if not self.pg_store:
             raise ConnectionError("PostgreSQL Store not initialized (Strict Mode)")
             
        episodes = self.pg_store.search_episodes(
            min_importance=score_threshold,
            limit=20
        )
        return [self._episode_to_memory(e) for e in episodes]

    def delete(self, memory_id: str):
        """Delete a memory."""
        pass # Not supported in V1 Strict API

    def stats(self) -> Dict:
        """Get storage statistics."""
        if not self.pg_store:
             raise ConnectionError("PostgreSQL Store not initialized (Strict Mode)")
        return self.pg_store.get_stats()

    def _episode_to_memory(self, ep) -> Memory:
        """Convert PG Episode to Memory object."""
        return Memory(
            id=ep.episode_id,
            content=ep.content,
            tier=MemoryTier.EPISODIC,
            score=ep.importance_score,
            domain=ep.source_type, 
            source=ep.source_ref or "observation",
            timestamp=ep.created_at,
            access_count=ep.consolidation_count,
            last_accessed=ep.accessed_at,
            metadata={"original_id": ep.episode_id}
        )


class SemanticMemoryStore:
    """
    MCP-based semantic memory storage.
    Integrates with MCP memory knowledge graph.
    """

    def __init__(self):
        self.bridge_log_path = Path(r"E:\genesis-system\data\semantic_memory_log.json")
        self._load_log()

    def _load_log(self):
        """Load pending operations log."""
        self.pending = {"entities": [], "relations": []}
        if self.bridge_log_path.exists():
            try:
                with open(self.bridge_log_path) as f:
                    self.pending = json.load(f)
            except Exception:
                pass

    def _save_log(self):
        """Save pending operations."""
        with open(self.bridge_log_path, 'w') as f:
            json.dump(self.pending, f, indent=2)

    def store(self, memory: Memory) -> str:
        """
        Queue memory for MCP storage.

        In Claude Code context, these will be synced via MCP tools.
        """
        entity = {
            "name": self._generate_entity_name(memory),
            "entityType": self._get_entity_type(memory.domain),
            "observations": [
                memory.content,
                f"Score: {memory.score:.3f}",
                f"Domain: {memory.domain}",
                f"Source: {memory.source}",
                f"Timestamp: {memory.timestamp}"
            ],
            "memory_id": memory.id
        }

        self.pending["entities"].append(entity)
        self._save_log()
        return memory.id

    def add_relation(self, from_memory: str, to_memory: str,
                     relation_type: str = "relates_to"):
        """Queue a relation between memories."""
        relation = {
            "from": from_memory,
            "to": to_memory,
            "relationType": relation_type
        }
        self.pending["relations"].append(relation)
        self._save_log()

    def get_pending(self) -> Dict:
        """Get pending MCP operations."""
        return self.pending

    def mark_synced(self):
        """Clear pending operations after sync."""
        self.pending = {"entities": [], "relations": []}
        self._save_log()

    def _generate_entity_name(self, memory: Memory) -> str:
        """Generate entity name from memory."""
        words = memory.content.split()[:6]
        short = " ".join(words)[:50]
        suffix = hashlib.md5(memory.id.encode()).hexdigest()[:4]
        return f"{memory.domain.title()}: {short} ({suffix})"

    def _get_entity_type(self, domain: str) -> str:
        """Map domain to entity type."""
        mapping = {
            "learning": "learning",
            "technical": "discovery",
            "error": "issue",
            "decision": "decision",
            "pattern": "pattern",
            "capability": "capability"
        }
        return mapping.get(domain, "observation")


class MemoryCortex:
    """
    Unified Memory Cortex - The Genesis Brain.

    Orchestrates all memory tiers:
    - Working Memory: Fast session cache
    - Episodic Memory: Experiential SQLite storage
    - Semantic Memory: MCP knowledge graph

    Uses surprise-based scoring for tier routing.
    """

    # Tier thresholds
    THRESHOLDS = {
        "discard": 0.3,
        "working": 0.5,
        "episodic": 0.8,
        "semantic": 0.8  # Same as episodic, but also goes to MCP
    }

    def __init__(self, enable_vectors: bool = True):
        # Initialize memory components
        self.surprise = MemorySystem()
        self.working = WorkingMemoryCache()
        self.episodic = EpisodicMemoryStore()
        self.semantic = SemanticMemoryStore()

        # Initialize vector backends if available
        self.vectors = None
        self.vector_enabled = False
        if enable_vectors and VECTOR_AVAILABLE:
            try:
                self.vectors = VectorManager()
                from synapse_linker import SynapseLinker
                self.linker = SynapseLinker(self.vectors)
                health = self.vectors.health()  # VectorManager uses health() not health_check()
                # Enable if at least one backend is healthy
                self.vector_enabled = any(health.values())
                if self.vector_enabled:
                    self._active_backends = [k for k, v in health.items() if v]
                else:
                    self._active_backends = []
            except Exception as e:
                self._active_backends = []
                print(f"Vector backends unavailable: {e}")

        # Stats tracking
        self.stats = {
            "memories_processed": 0,
            "tier_distribution": {t.value: 0 for t in MemoryTier},
            "vector_stores": 0
        }

    def remember(self, content: str, source: str = "claude_code",
                 domain: str = "general", metadata: Optional[Dict] = None,
                 force_tier: MemoryTier = None) -> Dict[str, Any]:
        """
        Process and store a memory.
        
        CRITICAL: Checks for SYSTEM_LOCKDOWN before executing.
        """
        if metadata is None:
            metadata = {}
        # 0. CARDIAC ARREST CHECK
        if os.path.exists("E:/genesis-system/data/SYSTEM_LOCKDOWN.mode"):
            print("⛔ MEMORY CORTEX REFUSED: System in Cardiac Arrest.")
            raise ConnectionError("SYSTEM HALTED: Core Connection Lost.")

        # 1. DEFENSIVE VALIDATION (Phase 1)
        try:
            from memory_schemas import MemoryItemInput
            validated = MemoryItemInput(content=content, source=source, domain=domain, metadata=metadata or {})
            content = validated.content # Use sanitized content
            relations = validated.relations # Extract relations
        except Exception as e:
            print(f"⛔ POISON PILL DETECTED: {e}")
            raise ValueError(f"Memory Reject: {e}")

        # Generate memory ID
        memory_id = self._generate_id(content)
        
        # 2. SYNAPSE LINKING (Phase 2)
        if relations and self.vector_enabled:
             # Conceptual: In a real graph DB we'd create edges here.
             # For Qdrant: We store them in metadata for now.
             metadata["relations"] = relations
             print(f"🕸️ SYNAPSE: Linking {memory_id} to {len(relations)} nodes.")
        timestamp = datetime.now().isoformat()

        # Calculate surprise score
        eval_result = self.surprise.evaluate(content, source, domain)
        score = eval_result["score"]["total"]

        # Determine tier
        if force_tier:
            tier = force_tier
        else:
            tier = self._route_to_tier(score)

        # Create memory object
        memory = Memory(
            id=memory_id,
            content=content,
            tier=tier,
            score=score,
            domain=domain,
            source=source,
            timestamp=timestamp,
            metadata=metadata or {}
        )

        # Store based on tier
        stored_in = []

        if tier == MemoryTier.DISCARD:
            pass  # Not stored

        elif tier == MemoryTier.WORKING:
            self.working.set(memory)
            stored_in.append("working_cache")

        elif tier == MemoryTier.EPISODIC:
            self.episodic.store(memory)
            stored_in.append("episodic_db")
            # Also store in MCP for high episodic
            if score >= 0.6:
                self.semantic.store(memory)
                stored_in.append("semantic_mcp")

        elif tier == MemoryTier.SEMANTIC:
            self.episodic.store(memory)  # Also in episodic for retrieval
            self.semantic.store(memory)
            stored_in.append("episodic_db")
            stored_in.append("semantic_mcp")

        # Store in vector backends for episodic and semantic tiers
        vector_results = {}
        if self.vector_enabled and tier in [MemoryTier.EPISODIC, MemoryTier.SEMANTIC]:
            try:
                vector_metadata = {
                    "tier": tier.value,
                    "score": score,
                    "domain": domain,
                    "source": source
                }
                
                # 3. SYNAPSE LINKING & RISK ASSESSMENT (TASK-007)
                if hasattr(self, "linker"):
                    links, risk = self.linker.link_memory(memory_id, content, domain)
                    vector_metadata["relations"] = links
                    vector_metadata["risk_level"] = risk
                    metadata["relations"] = links # Sync back to main metadata
                    metadata["risk_level"] = risk
                
                vector_results = self.vectors.add(
                    doc_id=memory_id,
                    content=content,
                    metadata=vector_metadata
                )
                print(f"      VECTOR DEBUG: results={vector_results}")
                for backend, success in vector_results.items():
                    if backend != "id" and success:
                        stored_in.append(f"vector_{backend}")
                        self.stats["vector_stores"] += 1
            except Exception as e:
                print(f"      VECTOR ERROR: {e}")
                vector_results["error"] = str(e)

        # Update stats
        self.stats["memories_processed"] += 1
        self.stats["tier_distribution"][tier.value] += 1

        return {
            "memory_id": memory_id,
            "tier": tier.value,
            "score": round(score, 3),
            "score_breakdown": eval_result["score"],
            "stored_in": stored_in,
            "timestamp": timestamp
        }

    def recall(self, query: str, limit: int = 10,
               tiers: List[MemoryTier] = None,
               use_vectors: bool = True) -> List[Dict]:
        """
        Search for relevant memories across all tiers.

        Uses hybrid search: keyword (FTS) + semantic (vectors).
        Returns ranked list of matching memories.
        """
        if tiers is None:
            tiers = [MemoryTier.WORKING, MemoryTier.EPISODIC, MemoryTier.SEMANTIC]

        results = []
        seen_ids = set()  # Dedupe across sources

        # Vector search first (if enabled) - provides semantic similarity
        if use_vectors and self.vector_enabled:
            try:
                vector_results = self.vectors.unified_search(query, top_k=limit)
                for doc in vector_results:
                    if doc.id not in seen_ids:
                        seen_ids.add(doc.id)
                        similarity = doc.score if doc.score is not None else 0.5
                        results.append({
                            "tier": "vector",
                            "source": doc.metadata.get("backend", "unknown"),
                            "memory": {
                                "id": doc.id,
                                "content": doc.content,
                                "score": doc.metadata.get("score", 0.5),
                                "domain": doc.metadata.get("domain", "general"),
                                "metadata": doc.metadata
                            },
                            "relevance": similarity,
                            "similarity": similarity
                        })
            except Exception as e:
                pass  # Fall through to keyword search

        # Search working memory
        if MemoryTier.WORKING in tiers:
            working_results = self.working.search(query, limit)
            for m in working_results:
                if m.id not in seen_ids:
                    seen_ids.add(m.id)
                    results.append({
                        "tier": "working",
                        "memory": m.to_dict(),
                        "relevance": 0.8  # Working memory is recent, high relevance
                    })

        # Search episodic memory (FTS)
        if MemoryTier.EPISODIC in tiers or MemoryTier.SEMANTIC in tiers:
            episodic_results = self.episodic.search(query, limit)
            for m in episodic_results:
                if m.id not in seen_ids:
                    seen_ids.add(m.id)
                    results.append({
                        "tier": "episodic",
                        "memory": m.to_dict(),
                        "relevance": m.score  # Use surprise score as relevance
                    })

        # Sort by relevance and return top results
        results.sort(key=lambda x: x["relevance"], reverse=True)
        return results[:limit]

    def consolidate(self) -> Dict[str, int]:
        """
        Run consolidation loop:
        1. Promote working → episodic based on access
        2. Promote episodic → semantic based on score/access
        """
        promoted = {"working_to_episodic": 0, "episodic_to_semantic": 0}

        # Promote from working memory
        promotable = self.working.get_promotable(access_threshold=3)
        for memory in promotable:
            # COMPLIANT: Only working tier can promote to episodic
            assert memory.tier == MemoryTier.WORKING, f"Promotion violation: {memory.tier} -> Episodic"
            memory.tier = MemoryTier.EPISODIC
            self.episodic.store(memory)
            self.working.remove(memory.id)
            promoted["working_to_episodic"] += 1

        # Promote from episodic to semantic
        promotable = self.episodic.get_promotable(
            score_threshold=0.75,
            access_threshold=5
        )
        for memory in promotable:
            if memory.tier != MemoryTier.SEMANTIC:
                # COMPLIANT: Only episodic tier can promote to semantic
                assert memory.tier == MemoryTier.EPISODIC, f"Promotion violation: {memory.tier} -> Semantic"
                memory.tier = MemoryTier.SEMANTIC
                self.semantic.store(memory)
                promoted["episodic_to_semantic"] += 1

        return promoted

    def reflect(self, action: str, expected: str, actual: str) -> List[Dict]:
        """
        Reflective learning from action outcomes.

        Stores significant deviations as learnings.
        """
        # Record observation
        self.surprise.observe(action, expected)

        # Reflect on outcome
        learnings = self.surprise.reflect(actual)

        # Process learnings through cortex
        results = []
        for learning in learnings:
            if learning.get("deviation", 0) > 0.4:
                result = self.remember(
                    content=f"Learning from '{action}': Expected '{expected}' but got '{actual}'",
                    source="reflective_loop",
                    domain="learning",
                    metadata={
                        "action": action,
                        "expected": expected,
                        "actual": actual,
                        "deviation": learning["deviation"]
                    }
                )
                results.append(result)

        return results

    def get_stats(self) -> Dict:
        """Get comprehensive memory statistics."""
        stats = {
            "cortex": self.stats,
            "working_memory": self.working.stats(),
            "episodic_memory": self.episodic.stats(),
            "pending_semantic_sync": {
                "entities": len(self.semantic.pending.get("entities", [])),
                "relations": len(self.semantic.pending.get("relations", []))
            },
            "surprise_memory": self.surprise.get_stats()
        }

        # Add vector backend stats
        if self.vector_enabled:
            stats["vector_backends"] = {
                "enabled": True,
                "active_backends": self._active_backends,
                "health": self.vectors.health()
            }
        else:
            stats["vector_backends"] = {
                "enabled": False,
                "reason": "No backends available or disabled"
            }

        return stats

    def sync_to_mcp(self) -> Dict:
        """
        Get commands to sync semantic memories to MCP.

        Run the returned commands in Claude Code context.
        """
        pending = self.semantic.get_pending()

        if not pending["entities"] and not pending["relations"]:
            return {"status": "nothing_to_sync"}

        commands = []

        if pending["entities"]:
            entities = [{
                "name": e["name"],
                "entityType": e["entityType"],
                "observations": e["observations"]
            } for e in pending["entities"]]
            commands.append({
                "tool": "mcp__memory__create_entities",
                "entities": entities
            })

        if pending["relations"]:
            relations = [{
                "from": r["from"],
                "to": r["to"],
                "relationType": r["relationType"]
            } for r in pending["relations"]]
            commands.append({
                "tool": "mcp__memory__create_relations",
                "relations": relations
            })

        return {
            "status": "pending_sync",
            "commands": commands,
            "entity_count": len(pending["entities"]),
            "relation_count": len(pending["relations"])
        }

    def mark_mcp_synced(self):
        """Mark all pending MCP operations as complete."""
        self.semantic.mark_synced()

    def _route_to_tier(self, score: float) -> MemoryTier:
        """Determine memory tier based on score."""
        if score < self.THRESHOLDS["discard"]:
            return MemoryTier.DISCARD
        elif score < self.THRESHOLDS["working"]:
            return MemoryTier.WORKING
        elif score < self.THRESHOLDS["episodic"]:
            return MemoryTier.EPISODIC
        else:
            return MemoryTier.SEMANTIC

    def _generate_id(self, content: str) -> str:
        """Generate unique memory ID (UUID v5)."""
        import uuid
        # Namespace for Genesis memory (Must match VectorManager)
        GENESIS_NAMESPACE = uuid.UUID('e2a6d7f8-b3c4-4d5e-8f90-a1b2c3d4e5f6')
        return str(uuid.uuid5(GENESIS_NAMESPACE, content))


# CLI Interface
if __name__ == "__main__":
    import sys

    cortex = MemoryCortex()

    if len(sys.argv) < 2:
        print("""
Genesis Memory Cortex - World-Leading AI Memory System
=======================================================

Commands:
  remember "<content>"          Store a new memory
  recall "<query>"              Search for memories
  reflect "<action>" "<exp>" "<actual>"  Learn from outcome
  consolidate                   Run memory consolidation
  sync                          Get MCP sync commands
  stats                         Show memory statistics

Examples:
  python genesis_memory_cortex.py remember "Discovered that caching improves latency by 50%"
  python genesis_memory_cortex.py recall "performance optimization"
  python genesis_memory_cortex.py reflect "Deploy feature" "Smooth rollout" "Had errors"
  python genesis_memory_cortex.py stats
        """)
        sys.exit(0)

    command = sys.argv[1]

    if command == "remember" and len(sys.argv) > 2:
        content = " ".join(sys.argv[2:])
        result = cortex.remember(content)
        print(json.dumps(result, indent=2))

    elif command == "recall" and len(sys.argv) > 2:
        query = " ".join(sys.argv[2:])
        results = cortex.recall(query)
        print(json.dumps(results, indent=2))

    elif command == "reflect" and len(sys.argv) >= 5:
        action = sys.argv[2]
        expected = sys.argv[3]
        actual = " ".join(sys.argv[4:])
        results = cortex.reflect(action, expected, actual)
        print(json.dumps(results, indent=2))

    elif command == "consolidate":
        result = cortex.consolidate()
        print(json.dumps(result, indent=2))

    elif command == "sync":
        result = cortex.sync_to_mcp()
        print(json.dumps(result, indent=2))

    elif command == "stats":
        stats = cortex.get_stats()
        print(json.dumps(stats, indent=2))

    else:
        print(f"Unknown command: {command}")
        sys.exit(1)
