"""
MCP (Model Context Protocol) server wrapper for KuzuMemory.

Provides MCP tools that wrap kuzu-memory CLI commands for seamless
integration with Claude Desktop and other MCP-compatible AI systems.
"""

import asyncio
import json
import logging
import os
import sys
from pathlib import Path
from typing import TYPE_CHECKING, Any

from pydantic import AnyUrl

from kuzu_memory.__version__ import __version__
from kuzu_memory.core.models import MemoryType

if TYPE_CHECKING:
    pass

# MCP SDK imports (will be dynamically imported if available)
try:
    from mcp.server import NotificationOptions, Server
    from mcp.server.models import InitializationOptions
    from mcp.server.stdio import stdio_server
    from mcp.types import Resource, ResourceTemplate, TextContent, Tool

    MCP_AVAILABLE = True
except ImportError:
    MCP_AVAILABLE = False

# Ensure MCP types are always bound for Pyright. mcp is a declared project
# dependency so these succeed in the venv; TYPE_CHECKING=False at runtime
# means this block never re-executes when the try block already succeeded.
if TYPE_CHECKING:
    from mcp.server import NotificationOptions, Server
    from mcp.server.models import InitializationOptions
    from mcp.server.stdio import stdio_server
    from mcp.types import Resource, ResourceTemplate, TextContent, Tool


logger = logging.getLogger(__name__)


class KuzuMemoryMCPServer:
    """
    MCP server that exposes kuzu-memory functionality as tools.

    This server provides:
    - enhance: Enhance prompts with project context
    - learn: Store learnings asynchronously
    - recall: Query specific memories
    - stats: Get memory system statistics
    - remember: Store new memories
    """

    def __init__(self, project_root: Path | None = None) -> None:
        """
        Initialize the MCP server.

        Args:
            project_root: Project root directory (auto-detected if not provided)
        """
        if not MCP_AVAILABLE:
            raise ImportError("MCP SDK is not installed. Install with: pip install mcp")

        self.project_root = project_root or self._find_project_root()
        self.server = Server("kuzu-memory")
        self.queue_processor: Any = None  # Initialized in run()
        self._setup_handlers()

    def _find_project_root(self) -> Path:
        """Find the project root directory."""
        # Check environment variable
        if "KUZU_MEMORY_PROJECT" in os.environ:
            return Path(os.environ["KUZU_MEMORY_PROJECT"])

        # Walk up from current directory
        current = Path.cwd()
        while current != current.parent:
            if (
                (current / ".git").exists()
                or (current / ".kuzu-memory").exists()
                or (current / "kuzu-memories").exists()  # legacy fallback
            ):
                return current
            current = current.parent

        # Default to current directory
        return Path.cwd()

    def _setup_handlers(self) -> None:
        """Set up MCP server handlers."""

        @self.server.list_tools()  # type: ignore[misc]
        async def handle_list_tools() -> list[Tool]:
            """List available tools."""
            return [
                Tool(
                    name="kuzu_enhance",
                    description=(
                        "RAG prompt augmentation: Enhance prompts with project-specific context "
                        "from KuzuMemory using semantic search and vector similarity. Performs "
                        "context injection by retrieving relevant project memories, patterns, and "
                        "learnings to augment the input prompt. Use this for context-aware AI "
                        "responses that understand project history and domain knowledge."
                    ),
                    inputSchema={
                        "type": "object",
                        "properties": {
                            "prompt": {
                                "type": "string",
                                "description": "The prompt to enhance with context",
                            },
                            "max_memories": {
                                "type": "integer",
                                "description": "Maximum number of memories to include (default: 5)",
                                "default": 5,
                            },
                        },
                        "required": ["prompt"],
                    },
                ),
                Tool(
                    name="kuzu_learn",
                    description=(
                        "ASYNC/BACKGROUND/NON-BLOCKING continuous learning: Store observations, "
                        "insights, and learnings asynchronously during conversations without waiting "
                        "for confirmation. Ideal for capturing context, patterns, and evolving "
                        "understanding as they emerge. Returns immediately without blocking. "
                        "\n\nWhen to use: Ongoing conversation learnings, observations, insights, "
                        "context capture during development sessions. "
                        "\n\nWhen NOT to use: Critical facts requiring immediate confirmation (use "
                        "kuzu_remember instead for synchronous storage of important decisions, "
                        "preferences, or facts that must be stored immediately)."
                    ),
                    inputSchema={
                        "type": "object",
                        "properties": {
                            "content": {
                                "type": "string",
                                "description": "The content to learn and store asynchronously",
                            },
                            "source": {
                                "type": "string",
                                "description": "Source of the learning (default: ai-conversation)",
                                "default": "ai-conversation",
                            },
                        },
                        "required": ["content"],
                    },
                ),
                Tool(
                    name="kuzu_recall",
                    description=(
                        "Semantic memory retrieval: Query project memories using vector search and "
                        "similarity matching. Performs semantic search across stored learnings, "
                        "patterns, decisions, and context to find relevant information based on "
                        "meaning rather than exact keyword matches. Returns memories ranked by "
                        "relevance score and temporal decay weighting. Use this to retrieve "
                        "project-specific knowledge, past decisions, learned patterns, or context "
                        "from previous conversations."
                    ),
                    inputSchema={
                        "type": "object",
                        "properties": {
                            "query": {
                                "type": "string",
                                "description": "Semantic query to search memories (meaning-based, not keyword matching)",
                            },
                            "limit": {
                                "type": "integer",
                                "description": "Maximum number of results to return (default: 5, higher values return more memories)",
                                "default": 5,
                            },
                        },
                        "required": ["query"],
                    },
                ),
                Tool(
                    name="kuzu_remember",
                    description=(
                        "SYNC/IMMEDIATE/BLOCKING critical fact storage: Store important decisions, "
                        "preferences, or facts that must be confirmed immediately. This operation "
                        "waits for database confirmation before returning, ensuring the memory is "
                        "durably persisted. Use for critical information that requires immediate "
                        "storage verification. "
                        "\n\nWhen to use: Important decisions, user preferences, project constraints, "
                        "architectural choices, critical facts that need immediate confirmation. "
                        "\n\nFor background learning during conversations: Use kuzu_learn instead "
                        "(async, non-blocking, ideal for continuous context capture without waiting)."
                    ),
                    inputSchema={
                        "type": "object",
                        "properties": {
                            "content": {
                                "type": "string",
                                "description": "The critical content to store immediately with confirmation",
                            },
                            "memory_type": {
                                "type": "string",
                                "description": "Type of memory: identity (project identity/context), preference (user preferences), decision (architectural/technical decisions), pattern (code patterns/conventions)",
                                "enum": [
                                    "identity",
                                    "preference",
                                    "decision",
                                    "pattern",
                                ],
                                "default": "identity",
                            },
                            "knowledge_type": {
                                "type": "string",
                                "description": (
                                    "Knowledge classification for cross-project promotion. "
                                    "Memories with knowledge_type in [rule, pattern, gotcha, architecture] "
                                    "and importance >= 0.8 are automatically promoted to the user-level "
                                    "DB (~/.kuzu-memory/user.db) at session end when user mode is enabled. "
                                    "Use: rule (hard constraints), pattern (reusable solutions), "
                                    "gotcha (pitfalls to avoid), architecture (structural decisions), "
                                    "convention (style choices, project-only), note (default, project-only)."
                                ),
                                "enum": [
                                    "rule",
                                    "pattern",
                                    "gotcha",
                                    "architecture",
                                    "convention",
                                    "note",
                                ],
                                "default": "note",
                            },
                            "importance": {
                                "type": "number",
                                "description": (
                                    "Memory importance score 0.0-1.0 (default: 0.8). "
                                    "Memories with importance >= 0.8 AND knowledge_type in "
                                    "[rule, pattern, gotcha, architecture] are promoted to user DB."
                                ),
                                "default": 0.8,
                                "minimum": 0.0,
                                "maximum": 1.0,
                            },
                        },
                        "required": ["content"],
                    },
                ),
                Tool(
                    name="kuzu_stats",
                    description=(
                        "Health check and diagnostics: Get KuzuMemory system statistics, health "
                        "metrics, and monitoring data. Returns memory counts by type, database size, "
                        "index health status, recent activity summary, and performance statistics. "
                        "Use this for system health monitoring, troubleshooting, capacity planning, "
                        "and understanding memory system usage patterns. "
                        "\n\nMetrics returned: Total memory count, memory type distribution "
                        "(identity/preference/decision/pattern), database storage size, recent "
                        "activity timestamp, and optionally (with detailed=true): average recall "
                        "time, cache hit rate, embedding generation performance, and query "
                        "optimization statistics."
                    ),
                    inputSchema={
                        "type": "object",
                        "properties": {
                            "detailed": {
                                "type": "boolean",
                                "description": "Show detailed statistics including performance metrics, cache statistics, and query optimization data (default: false)",
                                "default": False,
                            }
                        },
                    },
                ),
                Tool(
                    name="kuzu_optimize",
                    description=(
                        "LLM-initiated memory optimization: Compact and optimize frequently-accessed "
                        "memories to improve recall performance and reduce context clutter. Use "
                        "proactively when you notice redundant memories, stale content, or performance "
                        "degradation during conversations. "
                        "\n\nStrategies: "
                        "\n- full_maintenance: Run all three storage passes in sequence "
                        "(purge expired, deduplicate, trim oversized git metadata) — recommended first "
                        "\n- top_accessed: Refresh/consolidate most-used memories for faster recall "
                        "\n- stale_cleanup: Archive memories not accessed in 90+ days "
                        "\n- consolidate_similar: Merge similar memories into summaries "
                        "\n\nUse cases: "
                        "\n- full_maintenance first to recover storage and remove noise "
                        "\n- During long sessions when context feels cluttered "
                        "\n- When recall returns redundant results "
                        "\n- Proactive maintenance before ending session "
                        "\n- User requests memory cleanup"
                    ),
                    inputSchema={
                        "type": "object",
                        "properties": {
                            "strategy": {
                                "type": "string",
                                "description": "Optimization strategy to use",
                                "enum": [
                                    "full_maintenance",
                                    "top_accessed",
                                    "stale_cleanup",
                                    "consolidate_similar",
                                ],
                                "default": "top_accessed",
                            },
                            "limit": {
                                "type": "integer",
                                "description": "Maximum number of memories to process (default: 20)",
                                "default": 20,
                            },
                            "dry_run": {
                                "type": "boolean",
                                "description": "If true, only analyze without making changes (default: true)",
                                "default": True,
                            },
                        },
                    },
                ),
                Tool(
                    name="kuzu_merge",
                    description=(
                        "Merge memories from another Kùzu database into the current one. Imports new "
                        "memories while handling duplicates based on content hash and optional semantic "
                        "similarity. Use this to consolidate memories from different sources or backup "
                        "databases. "
                        "\n\nStrategies: "
                        "\n- skip: Ignore duplicate memories (safe default) "
                        "\n- update: Overwrite existing memories with metadata from source "
                        "\n- merge: Keep both versions with CONSOLIDATED_INTO relationship "
                        "\n\nUse cases: "
                        "\n- Importing memories from another project "
                        "\n- Consolidating memories after database split "
                        "\n- Restoring from backup with selective merge "
                        "\n- Migrating memories between environments"
                    ),
                    inputSchema={
                        "type": "object",
                        "properties": {
                            "source_path": {
                                "type": "string",
                                "description": "Path to the source Kùzu database directory to merge from",
                            },
                            "strategy": {
                                "type": "string",
                                "description": "Conflict resolution strategy: skip (ignore duplicates), update (overwrite with source), merge (keep both with relationship)",
                                "enum": ["skip", "update", "merge"],
                                "default": "skip",
                            },
                            "threshold": {
                                "type": "number",
                                "description": "Similarity threshold for deduplication (0.0-1.0, default: 0.95)",
                                "default": 0.95,
                            },
                            "dry_run": {
                                "type": "boolean",
                                "description": "If true, preview changes without modifying database (default: true)",
                                "default": True,
                            },
                            "backup": {
                                "type": "boolean",
                                "description": "Create backup of target database before merge (default: true)",
                                "default": True,
                            },
                        },
                        "required": ["source_path"],
                    },
                ),
                Tool(
                    name="kuzu_export_shared",
                    description=(
                        "Export delta memories to kuzu-memory-shared/ as JSON files for sharing "
                        "via git. Writes only user-generated memories (excludes git_sync source) "
                        "that are newer than the last export and at least min_age_days old. "
                        "Memories are grouped by creation date into memories-YYYY-MM-DD.json files "
                        "that can be committed to git and pulled by teammates or other machines. "
                        "\n\nUse cases: "
                        "\n- Share project learnings across machines "
                        "\n- Synchronise team knowledge via git "
                        "\n- Back up memories to a git-tracked location"
                    ),
                    inputSchema={
                        "type": "object",
                        "properties": {
                            "min_age_days": {
                                "type": "integer",
                                "description": "Minimum age of memories to export in days (default: 1, prevents intra-day exports)",
                                "default": 1,
                            },
                            "project_path": {
                                "type": "string",
                                "description": "Project root directory override (auto-detected if omitted)",
                            },
                        },
                    },
                ),
                Tool(
                    name="kuzu_import_shared",
                    description=(
                        "Import new memories from kuzu-memory-shared/ JSON files into the local "
                        "database. Scans memories-*.json files and inserts memories not already "
                        "present (deduplication by content_hash). "
                        "\n\nUse cases: "
                        "\n- Pull shared memories after a git pull "
                        "\n- Onboard a new machine with team knowledge "
                        "\n- Merge memories exported by a colleague"
                    ),
                    inputSchema={
                        "type": "object",
                        "properties": {
                            "project_path": {
                                "type": "string",
                                "description": "Project root directory override (auto-detected if omitted)",
                            },
                            "dry_run": {
                                "type": "boolean",
                                "description": "If true, preview what would be imported without making changes (default: false)",
                                "default": False,
                            },
                        },
                    },
                ),
                Tool(
                    name="kuzu_project_context",
                    description=(
                        "Structured project context bundle: Returns recent work summary, gotchas, "
                        "architecture decisions, patterns, rules, and conventions from the current "
                        "project's memory database. Designed for session-resume context injection. "
                        "\n\nUse cases: "
                        "\n- Inject project context at session start "
                        "\n- Resume work on a project with full context "
                        "\n- Get a structured overview of project knowledge"
                    ),
                    inputSchema={
                        "type": "object",
                        "properties": {
                            "days_back": {
                                "type": "integer",
                                "description": "How many days back to look for recent work",
                                "default": 14,
                            },
                            "max_per_type": {
                                "type": "integer",
                                "description": "Max memories per knowledge_type category",
                                "default": 5,
                            },
                        },
                    },
                ),
                Tool(
                    name="kuzu_user_context",
                    description=(
                        "Cross-project context bundle (user mode only): Returns rules, patterns, "
                        "gotchas, and architecture decisions from the shared user DB "
                        "(~/.kuzu-memory/user.db), excluding memories from the current project. "
                        "Complements kuzu_project_context for full session context. "
                        "\n\nOnly available when mode=user is configured. Returns "
                        '{"available": false} in project mode. '
                        "\n\nUse cases: "
                        "\n- Inject cross-project learnings at session start "
                        "\n- Apply patterns learned from other projects "
                        "\n- Access user-wide rules and conventions"
                    ),
                    inputSchema={
                        "type": "object",
                        "properties": {
                            "days_back": {
                                "type": "integer",
                                "description": "How many days back to look for recent work",
                                "default": 14,
                            },
                            "max_per_type": {
                                "type": "integer",
                                "description": "Max memories per knowledge_type category",
                                "default": 5,
                            },
                        },
                    },
                ),
            ]

        @self.server.call_tool()  # type: ignore[misc, unused-ignore]
        async def handle_call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
            """Handle tool calls."""

            if name == "kuzu_enhance":
                prompt = arguments.get("prompt", "")
                max_memories = arguments.get("max_memories", 5)
                result = await self._enhance(
                    str(prompt) if prompt is not None else "",
                    int(max_memories) if isinstance(max_memories, int) else 5,
                )
            elif name == "kuzu_learn":
                content = arguments.get("content", "")
                source = arguments.get("source", "ai-conversation")
                result = await self._learn(
                    str(content) if content is not None else "",
                    str(source) if source is not None else "ai-conversation",
                )
            elif name == "kuzu_recall":
                query = arguments.get("query", "")
                limit = arguments.get("limit", 5)
                result = await self._recall(
                    str(query) if query is not None else "",
                    int(limit) if isinstance(limit, int) else 5,
                )
            elif name == "kuzu_remember":
                content = arguments.get("content", "")
                memory_type = arguments.get("memory_type", "identity")
                knowledge_type = arguments.get("knowledge_type", None)
                importance = arguments.get("importance", None)
                result = await self._remember(
                    str(content) if content is not None else "",
                    str(memory_type) if memory_type is not None else "identity",
                    knowledge_type=str(knowledge_type) if knowledge_type is not None else None,
                    importance=float(importance) if importance is not None else None,
                )
            elif name == "kuzu_stats":
                detailed = arguments.get("detailed", False)
                result = await self._stats(bool(detailed) if detailed is not None else False)
            elif name == "kuzu_optimize":
                strategy = arguments.get("strategy", "top_accessed")
                limit = arguments.get("limit", 20)
                dry_run = arguments.get("dry_run", True)
                result = await self._optimize(
                    str(strategy) if strategy is not None else "top_accessed",
                    int(limit) if isinstance(limit, int) else 20,
                    bool(dry_run) if dry_run is not None else True,
                )
            elif name == "kuzu_merge":
                source_path = arguments.get("source_path", "")
                strategy = arguments.get("strategy", "skip")
                threshold = arguments.get("threshold", 0.95)
                dry_run = arguments.get("dry_run", True)
                backup = arguments.get("backup", True)
                result = await self._merge(
                    str(source_path) if source_path is not None else "",
                    str(strategy) if strategy is not None else "skip",
                    float(threshold) if isinstance(threshold, int | float) else 0.95,
                    bool(dry_run) if dry_run is not None else True,
                    bool(backup) if backup is not None else True,
                )
            elif name == "kuzu_export_shared":
                min_age_days = arguments.get("min_age_days", 1)
                project_path = arguments.get("project_path")
                result = await self._export_shared(
                    int(min_age_days) if isinstance(min_age_days, int) else 1,
                    str(project_path) if project_path is not None else None,
                )
            elif name == "kuzu_import_shared":
                project_path = arguments.get("project_path")
                dry_run = arguments.get("dry_run", False)
                result = await self._import_shared(
                    str(project_path) if project_path is not None else None,
                    bool(dry_run) if dry_run is not None else False,
                )
            elif name == "kuzu_project_context":
                days_back = arguments.get("days_back", 14)
                max_per_type = arguments.get("max_per_type", 5)
                result = await self._project_context(
                    int(days_back) if isinstance(days_back, int) else 14,
                    int(max_per_type) if isinstance(max_per_type, int) else 5,
                )
            elif name == "kuzu_user_context":
                days_back = arguments.get("days_back", 14)
                max_per_type = arguments.get("max_per_type", 5)
                result = await self._user_context(
                    int(days_back) if isinstance(days_back, int) else 14,
                    int(max_per_type) if isinstance(max_per_type, int) else 5,
                )
            else:
                result = f"Unknown tool: {name}"

            return [TextContent(type="text", text=result)]

        @self.server.list_resources()  # type: ignore[misc]
        async def handle_list_resources() -> list[Resource]:
            """List available resources."""
            return [
                Resource(
                    uri=AnyUrl(f"kuzu://project/{self.project_root.name}"),
                    name=f"Project: {self.project_root.name}",
                    description="KuzuMemory project context and memories",
                    mimeType="application/json",
                )
            ]

        @self.server.list_resource_templates()  # type: ignore[misc]
        async def handle_list_resource_templates() -> list[ResourceTemplate]:
            """List resource templates."""
            return [
                ResourceTemplate(
                    uriTemplate="kuzu://memory/{id}",
                    name="Memory by ID",
                    description="Access a specific memory by its ID",
                    mimeType="application/json",
                )
            ]

    async def _run_command(self, args: list[str], capture_output: bool = True) -> str:
        """
        Run a kuzu-memory command asynchronously.

        DEPRECATED: All tool methods now call the service layer directly via
        MemoryService, eliminating the ~50-300ms Python subprocess startup
        overhead per call.  This method is retained for potential future use
        (e.g. one-off admin commands) but is no longer called by any tool.
        """
        try:
            cmd = ["kuzu-memory", *args]

            if capture_output:
                process = await asyncio.create_subprocess_exec(
                    *cmd,
                    stdout=asyncio.subprocess.PIPE,
                    stderr=asyncio.subprocess.PIPE,
                    cwd=self.project_root,
                )
                stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=10.0)

                if process.returncode == 0:
                    return stdout.decode().strip()
                else:
                    error_msg = stderr.decode().strip() or "Command failed"
                    logger.error(f"Command failed: {' '.join(cmd)}: {error_msg}")
                    return f"Error: {error_msg}"
            else:
                # Fire and forget for async operations
                process = await asyncio.create_subprocess_exec(
                    *cmd,
                    stdout=asyncio.subprocess.DEVNULL,
                    stderr=asyncio.subprocess.DEVNULL,
                    cwd=self.project_root,
                )
                # Don't wait for completion
                return "Learning stored asynchronously"

        except TimeoutError:
            return "Error: Command timed out"
        except Exception as e:
            logger.error(f"Failed to run command: {e}")
            return f"Error: {e!s}"

    async def _enhance(self, prompt: str, max_memories: int = 5) -> str:
        """Enhance a prompt with project context via direct service call."""
        from kuzu_memory.services import MemoryService  # lazy import breaks circular dep

        if not prompt:
            return "Error: No prompt provided"

        db_path = self._get_db_path()
        if db_path.is_dir():
            db_path = db_path / "memories.db"
        try:
            with MemoryService(db_path=db_path, enable_git_sync=False) as memory:
                context = memory.attach_memories(
                    prompt,
                    max_memories=max_memories,
                    apply_temporal_decay=True,
                    use_semantic_search=True,
                )
            return context.enhanced_prompt or prompt
        except Exception as e:
            logger.error(f"MCP enhance failed: {e}")
            return f"Error: {e}"

    async def _learn(self, content: str, source: str = "ai-conversation") -> str:
        """Store a learning asynchronously via background learner (fire-and-forget)."""
        if not content:
            return "Error: No content provided"

        db_path = self._get_db_path()
        if db_path.is_dir():
            db_path = db_path / "memories.db"
        try:
            from ..async_memory.background_learner import get_background_learner
            from ..core.knowledge_classifier import classify_knowledge_type

            # Auto-classify so generated memories carry a meaningful knowledge_type.
            # The classified value is forwarded via metadata to the background task.
            auto_kt = classify_knowledge_type(content)
            learner = get_background_learner(db_path=db_path)
            task_id = learner.learn_async(
                content=content,
                source=source,
                metadata={"knowledge_type": auto_kt},
            )
            return f"Learning stored asynchronously (task {task_id[:8]}...)"
        except Exception as e:
            logger.error(f"MCP learn failed: {e}")
            return f"Error: {e}"

    async def _recall(self, query: str, limit: int = 5) -> str:
        """Query specific memories via direct service call."""
        from kuzu_memory.services import MemoryService  # lazy import breaks circular dep

        if not query:
            return "Error: No query provided"

        db_path = self._get_db_path()
        if db_path.is_dir():
            db_path = db_path / "memories.db"
        try:
            with MemoryService(db_path=db_path, enable_git_sync=False) as memory:
                context = memory.attach_memories(
                    query,
                    max_memories=limit,
                    apply_temporal_decay=True,
                    use_semantic_search=True,
                )
            memories = context.memories
            if not memories:
                return "No memories found"
            return "\n".join(f"- {m.content}" for m in memories)
        except Exception as e:
            logger.error(f"MCP recall failed: {e}")
            return f"Error: {e}"

    async def _remember(
        self,
        content: str,
        memory_type: str = "identity",
        knowledge_type: str | None = None,
        importance: float | None = None,
    ) -> str:
        """Store important project information via direct service call."""
        from kuzu_memory.services import MemoryService  # lazy import breaks circular dep

        if not content:
            return "Error: No content provided"

        db_path = self._get_db_path()
        if db_path.is_dir():
            db_path = db_path / "memories.db"
        try:
            with MemoryService(db_path=db_path, enable_git_sync=False) as memory:
                memory_id = memory.remember(
                    content,
                    source=memory_type,
                    knowledge_type=knowledge_type,
                    importance=importance,
                )
            return f"Stored memory with ID: {memory_id}"
        except Exception as e:
            logger.error(f"MCP remember failed: {e}")
            return f"Error: {e}"

    async def _stats(self, detailed: bool = False) -> str:
        """Get memory system statistics via direct service call."""
        from kuzu_memory.services import MemoryService  # lazy import breaks circular dep

        db_path = self._get_db_path()
        if db_path.is_dir():
            db_path = db_path / "memories.db"
        try:
            with MemoryService(db_path=db_path, enable_git_sync=False) as memory:
                total = memory.get_memory_count()
                db_size = memory.get_database_size()
                type_stats = memory.kuzu_memory.get_memory_type_stats()
                newest = memory.kuzu_memory.get_newest_memory_date()

            stats = []
            stats.append(f"Total Memories: {total}")
            stats.append(f"Memory Types: {type_stats}")
            stats.append(f"Recent Activity: {newest.isoformat() if newest else 'N/A'}")
            stats.append(f"Database Size: {db_size} bytes")

            if detailed:
                stats.append("Avg Recall Time: N/A")
                stats.append("Cache Hit Rate: N/A")

            return "\n".join(stats)
        except Exception as e:
            logger.error(f"MCP stats failed: {e}")
            return f"Error: {e}"

    async def _project_context(self, days_back: int = 14, max_per_type: int = 5) -> str:
        """Return structured project context bundle from the current project DB."""
        import datetime
        import json as _json

        from kuzu_memory.core.models import KnowledgeType
        from kuzu_memory.services import MemoryService

        db_path = self._get_db_path()
        if db_path.is_dir():
            db_path = db_path / "memories.db"
        try:
            with MemoryService(db_path=db_path, enable_git_sync=False) as memory:
                km = memory.kuzu_memory

                # Recent work — last 3 days of WORKING/EPISODIC memories
                recent_cutoff = (datetime.datetime.now() - datetime.timedelta(days=3)).isoformat()
                now_iso = datetime.datetime.now().isoformat()

                recent_rows = km.db_adapter.execute_query(
                    """MATCH (m:Memory)
                    WHERE m.created_at > TIMESTAMP($cutoff)
                      AND m.memory_type IN ['working', 'episodic']
                      AND (m.valid_to IS NULL OR m.valid_to > TIMESTAMP($now))
                    RETURN m ORDER BY m.created_at DESC LIMIT $limit""",
                    {
                        "cutoff": recent_cutoff,
                        "now": now_iso,
                        "limit": max_per_type * 2,
                    },
                )
                recent_summaries = []
                for row in recent_rows:
                    mem_data = row.get("m", row)
                    if isinstance(mem_data, dict):
                        recent_summaries.append(mem_data.get("content", ""))

                # Helper: query by knowledge_type
                def _by_type(kt: str) -> list[dict[str, object]]:
                    try:
                        rows = km.db_adapter.execute_query(
                            """MATCH (m:Memory)
                            WHERE m.knowledge_type = $kt
                              AND (m.valid_to IS NULL OR m.valid_to > TIMESTAMP($now))
                            RETURN m ORDER BY m.importance DESC LIMIT $limit""",
                            {
                                "kt": kt,
                                "now": now_iso,
                                "limit": max_per_type,
                            },
                        )
                        result = []
                        for r in rows:
                            mem_data = r.get("m", r)
                            if isinstance(mem_data, dict):
                                result.append(
                                    {
                                        "content": mem_data.get("content", ""),
                                        "importance": float(mem_data.get("importance", 0.5)),
                                    }
                                )
                        return result
                    except Exception as exc:
                        # Log a warning so schema issues surface rather than silently
                        # returning empty results.  The most common cause is the
                        # knowledge_type column being absent from an older database
                        # that was not yet migrated.
                        if (
                            "knowledge_type" in str(exc).lower()
                            or "cannot find property" in str(exc).lower()
                        ):
                            logger.warning(
                                "knowledge_type column missing from Memory table (kt=%s). "
                                "Schema migration may not have run yet. Error: %s",
                                kt,
                                exc,
                            )
                        else:
                            logger.debug("_by_type query failed for kt=%s: %s", kt, exc)
                        return []

                context = {
                    "recent_work": " ".join(recent_summaries[:3]) if recent_summaries else "",
                    "gotchas": _by_type(KnowledgeType.GOTCHA.value),
                    "architecture": _by_type(KnowledgeType.ARCHITECTURE.value),
                    "patterns": _by_type(KnowledgeType.PATTERN.value),
                    "rules": _by_type(KnowledgeType.RULE.value),
                    "conventions": _by_type(KnowledgeType.CONVENTION.value),
                }
            return _json.dumps(context, indent=2)
        except Exception as e:
            logger.error(f"MCP project_context failed: {e}")
            return _json.dumps({"error": str(e)})

    async def _user_context(self, days_back: int = 14, max_per_type: int = 5) -> str:
        """Return cross-project context from user DB (user mode only)."""
        import json as _json

        from kuzu_memory.core.config import KuzuMemoryConfig
        from kuzu_memory.services.user_memory_service import UserMemoryService

        try:
            # Load config to check mode
            from ..utils.config_loader import get_config_loader

            config_loader = get_config_loader()
            config: KuzuMemoryConfig = config_loader.load_config(self.project_root)
        except Exception:
            config = KuzuMemoryConfig()

        if config.user.mode != "user":
            return _json.dumps(
                {"available": False, "reason": "Not in user mode. Set mode: user in config."}
            )

        try:
            current_project_tag = config.user.effective_project_tag()
            with UserMemoryService(config) as user_svc:
                # Get patterns from user DB, excluding current project (already in project context)
                from kuzu_memory.core.models import KnowledgeType

                all_patterns = user_svc.get_patterns(limit=max_per_type * 6)
                # Filter out memories from the current project to avoid duplication
                cross_project = [m for m in all_patterns if m.project_tag != current_project_tag]

                def _filter_by_type(kt: KnowledgeType) -> list[dict[str, object]]:
                    return [
                        {"content": m.content, "importance": m.importance, "project": m.project_tag}
                        for m in cross_project
                        if m.knowledge_type == kt
                    ][:max_per_type]

                context = {
                    "available": True,
                    "source": "user_db",
                    "current_project_excluded": current_project_tag,
                    "gotchas": _filter_by_type(KnowledgeType.GOTCHA),
                    "architecture": _filter_by_type(KnowledgeType.ARCHITECTURE),
                    "patterns": _filter_by_type(KnowledgeType.PATTERN),
                    "rules": _filter_by_type(KnowledgeType.RULE),
                    "conventions": _filter_by_type(KnowledgeType.CONVENTION),
                }
            return _json.dumps(context, indent=2)
        except Exception as e:
            logger.error(f"MCP user_context failed: {e}")
            return _json.dumps({"available": False, "error": str(e)})

    def _get_db_path(self) -> Path:
        """Get path to Kuzu database file for current project.

        Runs migration opportunistically (kuzu-memories/ → .kuzu-memory/), then
        delegates to get_project_db_path() which returns the memories.db file path
        inside the canonical .kuzu-memory/ directory.

        The only case not handled by get_project_db_path() is the alternative
        hidden legacy directory .kuzu-memories/ (distinct from kuzu-memories/
        which migrate_db_location handles). That path is checked explicitly before
        delegating so no data is silently ignored.

        Search order:
        1. .kuzu-memory/memories.db  — new canonical dotfile location
        2. kuzu-memories/memories.db — legacy location (migrated by migrate_db_location)
        3. .kuzu-memories/memories.db — alternative hidden legacy location
        """
        from ..utils.project_setup import get_project_db_path, migrate_db_location

        # Run migration opportunistically (no-op if already migrated)
        migrate_db_location(self.project_root)

        # Alternative hidden legacy path not handled by migrate_db_location
        alt_legacy = self.project_root / ".kuzu-memories"
        if alt_legacy.exists() and not (self.project_root / ".kuzu-memory").exists():
            return alt_legacy / "memories.db"

        # Delegate to canonical helper which returns the .db file path
        return get_project_db_path(self.project_root)

    async def _optimize(self, strategy: str, limit: int, dry_run: bool) -> str:
        """
        Execute memory optimization strategy.

        Args:
            strategy: Optimization strategy (top_accessed, stale_cleanup,
                consolidate_similar, full_maintenance)
            limit: Maximum number of memories to process
            dry_run: If True, only analyze without making changes

        Returns:
            JSON-formatted optimization results
        """
        try:
            # Import required modules
            from ..core.config import KuzuMemoryConfig
            from ..monitoring.access_tracker import get_access_tracker
            from ..storage.kuzu_adapter import KuzuAdapter

            # Get database path
            db_path = self._get_db_path()

            # _get_db_path() may return a directory (e.g. when KUZU_MEMORY_DB is
            # set to a directory like /home/ubuntu/Vaults/notes/kuzu-memories).
            # KuzuAdapter requires a file path, so resolve memories.db inside the
            # directory — matching the behaviour of MemoryService and other tools.
            if db_path.is_dir():
                db_path = db_path / "memories.db"

            if not db_path.exists():
                return json.dumps(
                    {
                        "status": "error",
                        "error": "Memory database not found. Initialize with 'kuzu-memory setup' first.",
                    }
                )

            # Create database adapter
            config = KuzuMemoryConfig.default()
            db_adapter = KuzuAdapter(db_path, config)
            db_adapter.initialize()

            # Get access tracker
            tracker = get_access_tracker(db_path)

            # Backfill knowledge_type for existing memories that are still "note".
            # Runs before the selected strategy so downstream steps benefit from
            # the reclassified types.  Always runs regardless of dry_run because
            # it only updates memories that were incorrectly left as the default.
            backfill_result = await self._backfill_knowledge_types(db_adapter, dry_run)
            logger.info(
                "knowledge_type backfill: updated=%d skipped=%d dry_run=%s",
                backfill_result.get("updated", 0),
                backfill_result.get("skipped", 0),
                dry_run,
            )

            # Execute optimization based on strategy
            if strategy == "top_accessed":
                result = await self._optimize_top_accessed(db_adapter, tracker, limit, dry_run)
            elif strategy == "stale_cleanup":
                result = await self._optimize_stale_cleanup(db_adapter, limit, dry_run)
            elif strategy == "consolidate_similar":
                result = await self._optimize_consolidate_similar(db_adapter, limit, dry_run)
            elif strategy == "full_maintenance":
                result = await self._optimize_full_maintenance(db_adapter, dry_run)
            else:
                result = {
                    "status": "error",
                    "error": f"Unknown strategy: {strategy}",
                }

            return json.dumps(result, indent=2)

        except Exception as e:
            logger.error(f"Optimization failed: {e}", exc_info=True)
            return json.dumps({"status": "error", "error": str(e)})

    # ------------------------------------------------------------------
    # Storage maintenance helpers (used by full_maintenance strategy)
    # ------------------------------------------------------------------

    async def _maintenance_purge_expired(self, db_adapter: Any, dry_run: bool) -> dict[str, Any]:
        """
        Delete memories whose valid_to is in the past.

        Only memories with an explicit, non-NULL valid_to that is earlier than
        the current timestamp are removed.  NULL valid_to means permanent (no
        expiry) and is never touched.

        Args:
            db_adapter: Initialised KuzuAdapter
            dry_run: When True analyse only — no deletes are performed.

        Returns:
            Step result dict with keys: purged_count, dry_run.
        """
        import datetime

        now_iso = datetime.datetime.now().isoformat()

        try:
            # Count expired memories first (used for both dry-run and live run)
            count_rows = db_adapter.execute_query(
                """MATCH (m:Memory)
                WHERE m.valid_to IS NOT NULL
                  AND m.valid_to < TIMESTAMP($now)
                RETURN count(m) AS cnt""",
                {"now": now_iso},
            )
            expired_count: int = 0
            if count_rows:
                raw = count_rows[0].get("cnt", 0)
                expired_count = int(raw) if raw is not None else 0

            if not dry_run and expired_count > 0:
                db_adapter.execute_query(
                    """MATCH (m:Memory)
                    WHERE m.valid_to IS NOT NULL
                      AND m.valid_to < TIMESTAMP($now)
                    DELETE m""",
                    {"now": now_iso},
                )
                logger.info("Purged %d expired memories (valid_to < now)", expired_count)

            return {"purged_count": expired_count, "dry_run": dry_run}

        except Exception as e:
            logger.error("purge_expired failed: %s", e, exc_info=True)
            return {"purged_count": 0, "dry_run": dry_run, "error": str(e)}

    async def _maintenance_dedup(self, db_adapter: Any, dry_run: bool) -> dict[str, Any]:
        """
        Deduplicate memories by content hash.

        For every memory that lacks a content_hash, compute
        SHA-256(content) and write it back.  Then find groups sharing the
        same hash, keep the newest member of each group, and delete the
        older duplicates.

        Args:
            db_adapter: Initialised KuzuAdapter
            dry_run: When True analyse only — no writes/deletes are performed.

        Returns:
            Step result dict with keys: hashes_written, duplicates_removed, dry_run.
        """
        import hashlib

        hashes_written = 0
        duplicates_removed = 0

        try:
            # --- Step 1: back-fill missing content_hash values ---
            missing_rows = db_adapter.execute_query(
                """MATCH (m:Memory)
                WHERE m.content_hash IS NULL OR m.content_hash = ''
                RETURN m.id AS id, m.content AS content""",
                {},
            )

            for row in missing_rows:
                mem_id = row.get("id")
                content = row.get("content") or ""
                new_hash = hashlib.sha256(content.encode()).hexdigest()
                if not dry_run and mem_id:
                    try:
                        db_adapter.execute_query(
                            "MATCH (m:Memory {id: $id}) SET m.content_hash = $h",
                            {"id": str(mem_id), "h": new_hash},
                        )
                        hashes_written += 1
                    except Exception as e:
                        logger.warning("Failed to set content_hash for %s: %s", mem_id, e)
                else:
                    hashes_written += 1  # count as would-be-written in dry-run

            # --- Step 2: find duplicate groups ---
            # Fetch all (id, content_hash, created_at) so we can group in Python.
            # Kuzu does not support GROUP BY with aggregate keep-newest semantics
            # easily, so we do it client-side.
            all_rows = db_adapter.execute_query(
                """MATCH (m:Memory)
                WHERE m.content_hash IS NOT NULL AND m.content_hash <> ''
                RETURN m.id AS id, m.content_hash AS h, m.created_at AS created_at""",
                {},
            )

            from collections import defaultdict

            groups: dict[str, list[tuple[str, Any]]] = defaultdict(list)
            for row in all_rows:
                h = str(row.get("h") or "")
                mid = str(row.get("id") or "")
                ts = row.get("created_at")
                if h and mid:
                    groups[h].append((mid, ts))

            # For each group with > 1 member, delete all but the newest
            ids_to_delete: list[str] = []
            for _h, members in groups.items():
                if len(members) < 2:
                    continue

                # Sort descending by created_at; keep first (newest)
                def _ts_key(pair: tuple[str, Any]) -> Any:
                    ts = pair[1]
                    if ts is None:
                        return ""
                    return str(ts)

                members.sort(key=_ts_key, reverse=True)
                ids_to_delete.extend(mid for mid, _ in members[1:])

            duplicates_removed = len(ids_to_delete)

            if not dry_run and ids_to_delete:
                # Delete in batches of 100 to avoid oversized queries
                batch_size = 100
                for i in range(0, len(ids_to_delete), batch_size):
                    batch = ids_to_delete[i : i + batch_size]
                    try:
                        db_adapter.execute_query(
                            "MATCH (m:Memory) WHERE m.id IN $ids DELETE m",
                            {"ids": batch},
                        )
                    except Exception as e:
                        logger.warning("Batch dedup delete failed: %s", e)

                logger.info(
                    "Dedup: wrote %d hashes, removed %d duplicates",
                    hashes_written,
                    duplicates_removed,
                )

            return {
                "hashes_written": hashes_written,
                "duplicates_removed": duplicates_removed,
                "dry_run": dry_run,
            }

        except Exception as e:
            logger.error("dedup failed: %s", e, exc_info=True)
            return {
                "hashes_written": hashes_written,
                "duplicates_removed": duplicates_removed,
                "dry_run": dry_run,
                "error": str(e),
            }

    async def _maintenance_trim_git_metadata(
        self, db_adapter: Any, dry_run: bool
    ) -> dict[str, Any]:
        """
        Trim oversized metadata blobs on git_sync memories.

        Each git_sync memory can store a full ``changed_files`` list (up to
        146 items), per-file diff stats, and file category trees — all of
        which are never queried at recall time.  This step replaces those
        blobs with a slim subset:

            commit_sha, author, timestamp, summary (first line of message),
            files_changed_count (integer)

        The commit message content remains untouched in the ``content``
        column (that is what gets semantically searched).

        Args:
            db_adapter: Initialised KuzuAdapter
            dry_run: When True analyse only — no writes are performed.

        Returns:
            Step result dict with keys: trimmed_count, bytes_saved_estimate, dry_run.
        """
        # Threshold: metadata strings larger than 1 KB are candidates
        _METADATA_THRESHOLD = 1024

        trimmed_count = 0
        bytes_saved_estimate = 0

        try:
            rows = db_adapter.execute_query(
                """MATCH (m:Memory)
                WHERE m.source_type = 'git_sync'
                  AND m.metadata IS NOT NULL
                RETURN m.id AS id, m.metadata AS metadata""",
                {},
            )

            for row in rows:
                mem_id = str(row.get("id") or "")
                raw_meta = row.get("metadata") or ""
                if not mem_id or not raw_meta:
                    continue

                raw_str = str(raw_meta)
                if len(raw_str) <= _METADATA_THRESHOLD:
                    continue  # Already small enough

                # Parse metadata blob
                try:
                    if isinstance(raw_meta, dict):
                        meta: dict[str, Any] = raw_meta
                    else:
                        meta = json.loads(raw_str)
                except (json.JSONDecodeError, TypeError):
                    continue  # Can't parse — leave untouched

                # Build slim replacement
                slim: dict[str, Any] = {}
                if "commit_sha" in meta:
                    slim["commit_sha"] = meta["commit_sha"]
                if "commit_author" in meta:
                    slim["author"] = meta["commit_author"]
                elif "author" in meta:
                    slim["author"] = meta["author"]
                if "commit_timestamp" in meta:
                    slim["timestamp"] = meta["commit_timestamp"]
                # files_changed_count: prefer existing int, else len(changed_files)
                if "files_changed_count" in meta:
                    slim["files_changed_count"] = int(meta["files_changed_count"])
                elif "changed_files" in meta and isinstance(meta["changed_files"], list):
                    slim["files_changed_count"] = len(meta["changed_files"])
                # Preserve branch if present
                if "branch" in meta:
                    slim["branch"] = meta["branch"]

                slim_str = json.dumps(slim)
                old_size = len(raw_str)
                new_size = len(slim_str)

                if new_size >= old_size:
                    continue  # Nothing to save

                bytes_saved_estimate += old_size - new_size
                trimmed_count += 1

                if not dry_run:
                    try:
                        db_adapter.execute_query(
                            "MATCH (m:Memory {id: $id}) SET m.metadata = $meta",
                            {"id": mem_id, "meta": slim_str},
                        )
                    except Exception as e:
                        logger.warning("Failed to trim metadata for %s: %s", mem_id, e)
                        trimmed_count -= 1
                        bytes_saved_estimate -= old_size - new_size

            if not dry_run and trimmed_count > 0:
                logger.info(
                    "Trimmed git_sync metadata: %d records, ~%d bytes saved",
                    trimmed_count,
                    bytes_saved_estimate,
                )

            return {
                "trimmed_count": trimmed_count,
                "bytes_saved_estimate": bytes_saved_estimate,
                "dry_run": dry_run,
            }

        except Exception as e:
            logger.error("trim_git_metadata failed: %s", e, exc_info=True)
            return {
                "trimmed_count": trimmed_count,
                "bytes_saved_estimate": bytes_saved_estimate,
                "dry_run": dry_run,
                "error": str(e),
            }

    async def _backfill_knowledge_types(self, db_adapter: Any, dry_run: bool) -> dict[str, Any]:
        """
        Backfill knowledge_type for existing memories stored as "note" or NULL.

        Fetches memories with knowledge_type IN ('note', NULL), runs the
        rule-based classifier on each content string, and updates those whose
        classification changes to something more specific.

        Args:
            db_adapter: Open KuzuAdapter instance.
            dry_run: When True, compute changes but do not write to DB.

        Returns:
            Dict with keys "updated" (int), "skipped" (int), "dry_run" (bool).
        """
        from ..core.knowledge_classifier import classify_knowledge_type

        updated = 0
        skipped = 0

        try:
            # Fetch all note/null memories — no limit; backfill should be exhaustive.
            rows = db_adapter.execute_query(
                """MATCH (m:Memory)
                WHERE m.knowledge_type = 'note' OR m.knowledge_type IS NULL
                RETURN m.id AS id, m.content AS content""",
                {},
            )

            if not rows:
                return {"updated": 0, "skipped": 0, "dry_run": dry_run}

            for row in rows:
                mem_id = str(row.get("id") or "")
                content = str(row.get("content") or "")
                if not mem_id or not content:
                    skipped += 1
                    continue

                new_kt = classify_knowledge_type(content)
                if new_kt == "note":
                    # Classifier also returned note — nothing to change.
                    skipped += 1
                    continue

                updated += 1
                if not dry_run:
                    try:
                        db_adapter.execute_query(
                            "MATCH (m:Memory {id: $id}) SET m.knowledge_type = $kt",
                            {"id": mem_id, "kt": new_kt},
                        )
                    except Exception as exc:
                        logger.warning(
                            "Backfill: failed to update memory %s to %s: %s",
                            mem_id,
                            new_kt,
                            exc,
                        )
                        updated -= 1
                        skipped += 1

        except Exception as exc:
            logger.error("knowledge_type backfill failed: %s", exc, exc_info=True)

        return {"updated": updated, "skipped": skipped, "dry_run": dry_run}

    async def _optimize_full_maintenance(self, db_adapter: Any, dry_run: bool) -> dict[str, Any]:
        """
        Run all three storage maintenance passes in sequence.

        Steps:
        1. Purge expired memories (valid_to < now)
        2. Deduplicate by content_hash (back-fill missing hashes, then delete dupes)
        3. Trim oversized git_sync metadata blobs

        Args:
            db_adapter: Initialised KuzuAdapter
            dry_run: When True analyse only — no database mutations.

        Returns:
            Combined result dict summarising all three steps.
        """
        purge_result = await self._maintenance_purge_expired(db_adapter, dry_run)
        dedup_result = await self._maintenance_dedup(db_adapter, dry_run)
        trim_result = await self._maintenance_trim_git_metadata(db_adapter, dry_run)

        any_error = (
            purge_result.get("error") or dedup_result.get("error") or trim_result.get("error")
        )

        suggestions: list[str] = []
        purged = purge_result.get("purged_count", 0)
        dupes = dedup_result.get("duplicates_removed", 0)
        trimmed = trim_result.get("trimmed_count", 0)
        saved = trim_result.get("bytes_saved_estimate", 0)

        if purged:
            suggestions.append(
                f"{'Would remove' if dry_run else 'Removed'} {purged} expired memory record(s)"
            )
        if dupes:
            suggestions.append(
                f"{'Would remove' if dry_run else 'Removed'} {dupes} duplicate memory record(s)"
            )
        if trimmed:
            saved_kb = saved // 1024
            suggestions.append(
                f"{'Would trim' if dry_run else 'Trimmed'} {trimmed} git_sync metadata blob(s)"
                f" (~{saved_kb} KB)"
            )
        if not suggestions:
            suggestions.append("Database is already clean — nothing to do.")

        return {
            "status": "completed",
            "strategy": "full_maintenance",
            "dry_run": dry_run,
            "steps": {
                "purge_expired": purge_result,
                "dedup": dedup_result,
                "trim_git_metadata": trim_result,
            },
            "summary": {
                "expired_purged": purged,
                "duplicates_removed": dupes,
                "metadata_trimmed": trimmed,
                "bytes_saved_estimate": saved,
            },
            "suggestions": suggestions,
            **({"errors": any_error} if any_error else {}),
        }

    async def _optimize_top_accessed(
        self, db_adapter: Any, tracker: Any, limit: int, dry_run: bool
    ) -> dict[str, Any]:
        """
        Optimize top-accessed memories by consolidating similar ones.

        Strategy: Find frequently accessed memories and check for duplicates/similar content.
        """
        from ..utils.deduplication import DeduplicationEngine

        try:
            # Query top-accessed memories
            query = """
            MATCH (m:Memory)
            WHERE m.access_count > 5
            RETURN m
            ORDER BY m.access_count DESC
            LIMIT $limit
            """

            results = db_adapter.execute_query(query, {"limit": limit})

            if not results:
                return {
                    "status": "completed",
                    "strategy": "top_accessed",
                    "dry_run": dry_run,
                    "results": {
                        "memories_analyzed": 0,
                        "optimization_candidates": 0,
                        "actions_taken": [],
                        "space_saved_bytes": 0,
                        "estimated_recall_improvement": "0%",
                    },
                    "suggestions": ["No frequently accessed memories found."],
                }

            # Convert to Memory objects
            from ..core.models import Memory

            memories = []
            for row in results:
                memory_data = row.get("m")
                if memory_data:
                    try:
                        memory = Memory.from_dict(memory_data)
                        memories.append(memory)
                    except Exception as e:
                        logger.warning(f"Failed to parse memory: {e}")

            # Find similar memories among top accessed
            dedup_engine = DeduplicationEngine(
                exact_threshold=0.95, near_threshold=0.80, semantic_threshold=0.70
            )

            actions_taken = []
            optimization_candidates = 0

            for i, memory in enumerate(memories):
                remaining = memories[i + 1 :]
                if not remaining:
                    break

                duplicates = dedup_engine.find_duplicates(
                    memory.content, remaining, memory_type=memory.memory_type
                )

                if duplicates:
                    optimization_candidates += len(duplicates)
                    for dup_memory, score, match_type in duplicates:
                        actions_taken.append(
                            {
                                "action": "consolidation_candidate",
                                "memory_ids": [memory.id, dup_memory.id],
                                "similarity": f"{score:.2f}",
                                "match_type": match_type,
                            }
                        )

            suggestions = []
            if optimization_candidates > 0:
                suggestions.append(
                    f"Found {optimization_candidates} consolidation opportunities among top-accessed memories"
                )
                if dry_run:
                    suggestions.append("Run with dry_run=false to consolidate similar memories")
            else:
                suggestions.append(
                    "No similar memories found among top-accessed. Memory context is already optimized."
                )

            return {
                "status": "completed",
                "strategy": "top_accessed",
                "dry_run": dry_run,
                "results": {
                    "memories_analyzed": len(memories),
                    "optimization_candidates": optimization_candidates,
                    "actions_taken": actions_taken[:10],  # Limit output
                    "space_saved_bytes": 0,  # Would be calculated in non-dry-run
                    "estimated_recall_improvement": (
                        f"{min(15, optimization_candidates * 2)}%"
                        if optimization_candidates > 0
                        else "0%"
                    ),
                },
                "suggestions": suggestions,
            }

        except Exception as e:
            logger.error(f"Top-accessed optimization failed: {e}", exc_info=True)
            return {
                "status": "error",
                "strategy": "top_accessed",
                "error": str(e),
            }

    async def _optimize_stale_cleanup(
        self, db_adapter: Any, limit: int, dry_run: bool
    ) -> dict[str, Any]:
        """
        Clean up stale memories not accessed in 90+ days.

        Strategy: Use SmartPruningStrategy to identify and archive stale memories.
        """
        from ..core.smart_pruning import SmartPruningStrategy

        try:
            # Create pruning strategy focused on stale memories
            pruning_strategy = SmartPruningStrategy(
                db_adapter=db_adapter,
                threshold=0.3,  # Low threshold to catch stale memories
                archive_enabled=True,
            )

            # Get candidates
            candidates = pruning_strategy.get_prune_candidates()

            # Filter for stale memories (90+ days without access)
            from datetime import UTC, datetime, timedelta

            stale_threshold = datetime.now(UTC) - timedelta(days=90)
            stale_candidates = []

            for candidate in candidates:
                # Query memory details
                query = """
                MATCH (m:Memory {id: $id})
                RETURN m.accessed_at AS accessed_at, m.created_at AS created_at
                """
                result = db_adapter.execute_query(query, {"id": candidate.memory_id})

                if result:
                    accessed_at = result[0].get("accessed_at")
                    created_at = result[0].get("created_at")

                    # Parse timestamp
                    if accessed_at:
                        if isinstance(accessed_at, str):
                            accessed_at = datetime.fromisoformat(accessed_at.replace("Z", "+00:00"))
                    elif created_at:
                        if isinstance(created_at, str):
                            created_at = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
                        accessed_at = created_at

                    if accessed_at and accessed_at < stale_threshold:
                        stale_candidates.append(candidate)

            # Limit results
            stale_candidates = stale_candidates[:limit]

            actions_taken = []
            if not dry_run and stale_candidates:
                # Execute archiving and deletion
                pruning_strategy._archive_memories(stale_candidates)
                pruning_strategy._delete_memories([c.memory_id for c in stale_candidates])

                for candidate in stale_candidates:
                    actions_taken.append(
                        {
                            "action": "archived",
                            "memory_id": candidate.memory_id,
                            "reason": "stale (>90 days without access)",
                        }
                    )
            else:
                for candidate in stale_candidates:
                    actions_taken.append(
                        {
                            "action": "archive_candidate",
                            "memory_id": candidate.memory_id,
                            "reason": "stale (>90 days without access)",
                        }
                    )

            suggestions = []
            if stale_candidates:
                suggestions.append(
                    f"Found {len(stale_candidates)} stale memories ready for archiving"
                )
                if dry_run:
                    suggestions.append(
                        "Run with dry_run=false to archive these memories (30-day recovery window)"
                    )
                else:
                    suggestions.append("Archived memories can be restored within 30 days if needed")
            else:
                suggestions.append("No stale memories found. All memories are actively used.")

            return {
                "status": "completed",
                "strategy": "stale_cleanup",
                "dry_run": dry_run,
                "results": {
                    "memories_analyzed": len(candidates),
                    "optimization_candidates": len(stale_candidates),
                    "actions_taken": actions_taken[:10],  # Limit output
                    "space_saved_bytes": (
                        sum(len(c.memory_id) for c in stale_candidates) * 100
                    ),  # Estimate
                    "estimated_recall_improvement": (
                        f"{min(10, len(stale_candidates))}%" if stale_candidates else "0%"
                    ),
                },
                "suggestions": suggestions,
            }

        except Exception as e:
            logger.error(f"Stale cleanup optimization failed: {e}", exc_info=True)
            return {
                "status": "error",
                "strategy": "stale_cleanup",
                "error": str(e),
            }

    async def _optimize_consolidate_similar(
        self, db_adapter: Any, limit: int, dry_run: bool
    ) -> dict[str, Any]:
        """
        Consolidate similar memories into summaries.

        Strategy: Use ConsolidationEngine to cluster and merge similar memories.
        """
        from ..nlp.consolidation import ConsolidationEngine

        try:
            # Create consolidation engine
            consolidation_engine = ConsolidationEngine(
                db_adapter=db_adapter,
                similarity_threshold=0.70,
                min_age_days=30,  # Only consolidate memories older than 30 days
                max_access_count=5,  # Only consolidate low-access memories
            )

            # Execute consolidation
            result = consolidation_engine.execute(dry_run=dry_run, create_backup=False)

            actions_taken = []
            if result.clusters:
                for cluster in result.clusters[:limit]:
                    if dry_run:
                        actions_taken.append(
                            {
                                "action": "consolidation_candidate",
                                "memory_ids": [m.id for m in cluster.memories],
                                "cluster_size": len(cluster.memories),
                                "avg_similarity": f"{cluster.avg_similarity:.2f}",
                            }
                        )
                    else:
                        actions_taken.append(
                            {
                                "action": "consolidated",
                                "memory_ids": [m.id for m in cluster.memories],
                                "into": cluster.cluster_id,
                                "cluster_size": len(cluster.memories),
                            }
                        )

            suggestions = []
            if result.clusters_found > 0:
                suggestions.append(
                    f"Found {result.clusters_found} memory clusters that could be consolidated"
                )
                if dry_run:
                    suggestions.append(
                        "Run with dry_run=false to merge similar memories into summaries"
                    )
                else:
                    suggestions.append(
                        f"Successfully consolidated {result.memories_consolidated} memories into {result.new_memories_created} summaries"
                    )
            else:
                suggestions.append(
                    "No similar memory clusters found. Consider lowering similarity threshold."
                )

            return {
                "status": "completed",
                "strategy": "consolidate_similar",
                "dry_run": dry_run,
                "results": {
                    "memories_analyzed": result.memories_analyzed,
                    "optimization_candidates": result.clusters_found,
                    "actions_taken": actions_taken[:10],  # Limit output
                    "space_saved_bytes": result.memories_consolidated * 500,  # Estimate
                    "estimated_recall_improvement": (
                        f"{min(20, result.clusters_found * 3)}%"
                        if result.clusters_found > 0
                        else "0%"
                    ),
                },
                "suggestions": suggestions,
            }

        except Exception as e:
            logger.error(f"Consolidation optimization failed: {e}", exc_info=True)
            return {
                "status": "error",
                "strategy": "consolidate_similar",
                "error": str(e),
            }

    async def _merge(
        self,
        source_path: str,
        strategy: str,
        threshold: float,
        dry_run: bool,
        backup: bool,
    ) -> str:
        """
        Merge memories from another Kùzu database.

        Args:
            source_path: Path to source database directory
            strategy: Conflict resolution strategy (skip, update, merge)
            threshold: Similarity threshold for deduplication (0.0-1.0)
            dry_run: If True, preview changes without executing
            backup: If True, create backup before merge

        Returns:
            JSON-formatted merge results
        """
        import shutil
        import time
        import uuid
        from datetime import UTC, datetime
        from pathlib import Path

        try:
            import kuzu
        except ImportError:
            return json.dumps(
                {
                    "status": "error",
                    "error": "Kuzu library not found. Install with: pip install kuzu>=0.4.0",
                }
            )

        from ..core.config import KuzuMemoryConfig
        from ..core.models import Memory
        from ..storage.kuzu_adapter import KuzuAdapter
        from ..utils.deduplication import DeduplicationEngine

        try:
            # Validate inputs
            if not source_path:
                return json.dumps({"status": "error", "error": "source_path is required"})

            if not (0.0 <= threshold <= 1.0):
                return json.dumps(
                    {
                        "status": "error",
                        "error": f"threshold must be between 0.0 and 1.0, got {threshold}",
                    }
                )

            source_path_obj = Path(source_path)
            if not source_path_obj.exists():
                return json.dumps(
                    {
                        "status": "error",
                        "error": f"Source database not found: {source_path}",
                    }
                )

            # Get target database path
            target_path = self._get_db_path()
            if target_path.is_dir():
                target_path = target_path / "memories.db"
            if not target_path.exists():
                return json.dumps(
                    {
                        "status": "error",
                        "error": "Target database not found. Initialize with 'kuzu-memory setup' first.",
                    }
                )

            start_time = time.time()

            # Step 1: Read source database
            logger.info(f"Reading source database: {source_path_obj}")
            source_db = kuzu.Database(str(source_path_obj), read_only=True)
            source_conn = kuzu.Connection(source_db)

            source_query = """
                MATCH (m:Memory)
                RETURN m.id AS id,
                       m.content AS content,
                       m.content_hash AS content_hash,
                       m.created_at AS created_at,
                       m.memory_type AS memory_type,
                       m.importance AS importance,
                       m.confidence AS confidence,
                       m.source_type AS source_type,
                       m.agent_id AS agent_id,
                       m.user_id AS user_id,
                       m.session_id AS session_id,
                       m.metadata AS metadata,
                       m.accessed_at AS accessed_at,
                       m.access_count AS access_count
                ORDER BY m.created_at ASC
            """

            result = source_conn.execute(source_query)
            source_memories_raw = []
            # Handle single QueryResult (not a list)
            if not isinstance(result, list):
                column_names = result.get_column_names()
                while result.has_next():
                    row_list: list[Any] = list(result.get_next())
                    row_dict = {str(column_names[i]): row_list[i] for i in range(len(column_names))}
                    source_memories_raw.append(row_dict)

            if len(source_memories_raw) == 0:
                return json.dumps(
                    {
                        "status": "completed",
                        "message": "Source database is empty, nothing to merge",
                        "total_source": 0,
                        "duplicates_found": 0,
                        "new_imported": 0,
                        "updated": 0,
                        "skipped": 0,
                        "errors": 0,
                    }
                )

            # Step 2: Read target database
            logger.info(f"Reading target database: {target_path}")
            config = KuzuMemoryConfig.default()
            target_adapter = KuzuAdapter(target_path, config)
            target_adapter.initialize()

            with target_adapter._pool.get_connection() as target_conn:
                target_query = """
                    MATCH (m:Memory)
                    RETURN m.id AS id,
                           m.content AS content,
                           m.content_hash AS content_hash,
                           m.created_at AS created_at,
                           m.memory_type AS memory_type,
                           m.importance AS importance,
                           m.confidence AS confidence,
                           m.source_type AS source_type,
                           m.accessed_at AS accessed_at,
                           m.access_count AS access_count
                """
                result_target = target_conn.execute(target_query)
                target_memories_raw = []
                column_names_target = result_target.get_column_names()
                while result_target.has_next():
                    row_target = result_target.get_next()
                    row_dict_target = {
                        column_names_target[i]: row_target[i]
                        for i in range(len(column_names_target))
                    }
                    target_memories_raw.append(row_dict_target)

            # Convert to Memory objects
            target_memories = []
            target_hash_to_id = {}
            for row in target_memories_raw:
                # Convert memory_type string to MemoryType enum
                memory_type_str = str(row["memory_type"])
                try:
                    memory_type = MemoryType(memory_type_str)
                except ValueError:
                    # Fallback to EPISODIC if unknown type
                    memory_type = MemoryType.EPISODIC

                mem = Memory(
                    id=str(row["id"]),
                    content=str(row["content"]),
                    content_hash=str(row["content_hash"]),
                    memory_type=memory_type,
                    importance=float(row["importance"]) if row["importance"] else 0.5,
                    confidence=float(row["confidence"]) if row["confidence"] else 1.0,
                    source_type=str(row["source_type"]),
                    created_at=datetime.fromisoformat(
                        str(row["created_at"]).replace("Z", "+00:00")
                    ),
                    accessed_at=(
                        datetime.fromisoformat(str(row["accessed_at"]).replace("Z", "+00:00"))
                        if row["accessed_at"]
                        else None
                    ),
                    access_count=int(row["access_count"]) if row["access_count"] else 0,
                    # Add missing required fields with defaults
                    valid_to=None,  # No expiration
                    user_id=None,  # No user filtering
                    session_id=None,  # No session filtering
                )
                target_memories.append(mem)
                target_hash_to_id[mem.content_hash] = mem.id

            # Step 3: Deduplicate
            logger.info("Analyzing duplicates...")
            dedup = DeduplicationEngine(
                near_threshold=threshold,
                semantic_threshold=threshold,
                enable_update_detection=False,
            )

            # Type-annotate for mypy
            new_memories: list[dict[str, Any]] = []
            duplicate_memories: list[dict[str, Any]] = []
            id_mapping: dict[str, str] = {}

            for row in source_memories_raw:
                source_content = str(row["content"])
                source_content_hash = str(row["content_hash"])

                # Check exact hash match first
                if source_content_hash in target_hash_to_id:
                    duplicate_memories.append(
                        {
                            "source_row": row,
                            "target_id": target_hash_to_id[source_content_hash],
                            "match_type": "exact",
                            "similarity": 1.0,
                        }
                    )
                    id_mapping[str(row["id"])] = target_hash_to_id[source_content_hash]
                else:
                    # Check semantic similarity
                    duplicates = dedup.find_duplicates(source_content, target_memories)
                    if duplicates:
                        best_match, similarity, match_type = duplicates[0]
                        duplicate_memories.append(
                            {
                                "source_row": row,
                                "target_id": best_match.id,
                                "match_type": match_type,
                                "similarity": similarity,
                            }
                        )
                        id_mapping[str(row["id"])] = best_match.id
                    else:
                        # No duplicate - new memory
                        new_id = str(uuid.uuid4())
                        new_memories.append({"source_row": row, "new_id": new_id})
                        id_mapping[str(row["id"])] = new_id

            analysis_time_ms = (time.time() - start_time) * 1000

            # If dry-run, return preview
            if dry_run:
                return json.dumps(
                    {
                        "status": "preview",
                        "dry_run": True,
                        "total_source": len(source_memories_raw),
                        "duplicates_found": len(duplicate_memories),
                        "new_imported": len(new_memories),
                        "updated": 0,
                        "skipped": 0,
                        "errors": 0,
                        "analysis_time_ms": analysis_time_ms,
                        "strategy": strategy,
                        "threshold": threshold,
                        "message": f"Preview: {len(new_memories)} new memories would be imported, {len(duplicate_memories)} duplicates found",
                    },
                    indent=2,
                )

            # Execute merge
            # Backup if requested
            backup_path = None
            if backup:
                logger.info("Creating backup...")
                backup_path = target_path.parent / f"backup_{target_path.name}_{int(time.time())}"
                try:
                    shutil.copytree(target_path, backup_path)
                except Exception as e:
                    logger.warning(f"Backup failed: {e}")

            # Import new memories
            logger.info(f"Importing {len(new_memories)} new memories...")
            imported_count = 0

            with target_adapter._pool.get_connection() as target_conn:
                for mem_data in new_memories:
                    row = mem_data["source_row"]
                    new_id = mem_data["new_id"]

                    # Add merged_from to metadata
                    metadata_str = str(row.get("metadata", "{}"))
                    try:
                        metadata = json.loads(metadata_str) if metadata_str != "{}" else {}
                    except json.JSONDecodeError:
                        metadata = {}

                    metadata["merged_from"] = str(source_path)
                    metadata["merged_at"] = datetime.now(UTC).isoformat()
                    metadata["original_id"] = str(row["id"])

                    insert_query = """
                        CREATE (m:Memory {
                            id: $id,
                            content: $content,
                            content_hash: $content_hash,
                            created_at: $created_at,
                            memory_type: $memory_type,
                            importance: $importance,
                            confidence: $confidence,
                            source_type: $source_type,
                            agent_id: $agent_id,
                            user_id: $user_id,
                            session_id: $session_id,
                            metadata: $metadata,
                            accessed_at: $accessed_at,
                            access_count: $access_count,
                            valid_from: $created_at,
                            valid_to: NULL
                        })
                    """

                    created_at_dt = row["created_at"]
                    if isinstance(created_at_dt, str):
                        created_at_dt = datetime.fromisoformat(created_at_dt.replace("Z", "+00:00"))

                    accessed_at_dt = row.get("accessed_at")
                    if accessed_at_dt and isinstance(accessed_at_dt, str):
                        accessed_at_dt = datetime.fromisoformat(
                            accessed_at_dt.replace("Z", "+00:00")
                        )

                    params = {
                        "id": new_id,
                        "content": str(row["content"]),
                        "content_hash": str(row["content_hash"]),
                        "created_at": created_at_dt,
                        "memory_type": str(row["memory_type"]),
                        "importance": (float(row["importance"]) if row["importance"] else 0.5),
                        "confidence": (float(row["confidence"]) if row["confidence"] else 1.0),
                        "source_type": str(row["source_type"]),
                        "agent_id": str(row.get("agent_id", "default")),
                        "user_id": str(row["user_id"]) if row.get("user_id") else None,
                        "session_id": (str(row["session_id"]) if row.get("session_id") else None),
                        "metadata": json.dumps(metadata),
                        "accessed_at": accessed_at_dt,
                        "access_count": int(row.get("access_count", 0)),
                    }

                    target_conn.execute(insert_query, params)
                    imported_count += 1

            # Handle duplicates based on strategy
            updated_count = 0
            merged_count = 0
            skipped_count = 0

            if duplicate_memories:
                if strategy == "update":
                    logger.info(f"Updating {len(duplicate_memories)} duplicates...")
                    with target_adapter._pool.get_connection() as target_conn:
                        for dup in duplicate_memories:
                            row = dup["source_row"]
                            target_id = dup["target_id"]

                            metadata_str = str(row.get("metadata", "{}"))
                            try:
                                metadata = json.loads(metadata_str) if metadata_str != "{}" else {}
                            except json.JSONDecodeError:
                                metadata = {}

                            metadata["updated_from_merge"] = str(source_path)
                            metadata["updated_at"] = datetime.now(UTC).isoformat()

                            update_query = """
                                MATCH (m:Memory {id: $id})
                                SET m.importance = $importance,
                                    m.confidence = $confidence,
                                    m.metadata = $metadata,
                                    m.accessed_at = $now,
                                    m.access_count = m.access_count + 1
                            """

                            params = {
                                "id": target_id,
                                "importance": (
                                    float(row["importance"]) if row["importance"] else 0.5
                                ),
                                "confidence": (
                                    float(row["confidence"]) if row["confidence"] else 1.0
                                ),
                                "metadata": json.dumps(metadata),
                                "now": datetime.now(UTC),
                            }

                            target_conn.execute(update_query, params)
                            updated_count += 1

                elif strategy == "merge":
                    logger.info(
                        f"Creating CONSOLIDATED_INTO relationships for {len(duplicate_memories)} duplicates..."
                    )
                    with target_adapter._pool.get_connection() as target_conn:
                        for dup in duplicate_memories:
                            row = dup["source_row"]
                            target_id = dup["target_id"]

                            merge_id = str(uuid.uuid4())

                            metadata_str = str(row.get("metadata", "{}"))
                            try:
                                metadata = json.loads(metadata_str) if metadata_str != "{}" else {}
                            except json.JSONDecodeError:
                                metadata = {}

                            metadata["merged_from"] = str(source_path)
                            metadata["merged_at"] = datetime.now(UTC).isoformat()
                            metadata["consolidated_with"] = target_id

                            merge_query = """
                                MATCH (target:Memory {id: $target_id})
                                CREATE (merged:Memory {
                                    id: $merge_id,
                                    content: $content,
                                    content_hash: $content_hash,
                                    created_at: $created_at,
                                    memory_type: $memory_type,
                                    importance: $importance,
                                    confidence: $confidence,
                                    source_type: $source_type,
                                    agent_id: $agent_id,
                                    user_id: $user_id,
                                    session_id: $session_id,
                                    metadata: $metadata,
                                    accessed_at: NULL,
                                    access_count: 0,
                                    valid_from: $created_at,
                                    valid_to: NULL
                                })
                                CREATE (merged)-[:CONSOLIDATED_INTO {
                                    consolidation_date: $now,
                                    cluster_id: $cluster_id,
                                    similarity_score: $similarity
                                }]->(target)
                            """

                            created_at_dt = row["created_at"]
                            if isinstance(created_at_dt, str):
                                created_at_dt = datetime.fromisoformat(
                                    created_at_dt.replace("Z", "+00:00")
                                )

                            params = {
                                "target_id": target_id,
                                "merge_id": merge_id,
                                "content": str(row["content"]),
                                "content_hash": str(row["content_hash"]),
                                "created_at": created_at_dt,
                                "memory_type": str(row["memory_type"]),
                                "importance": (
                                    float(row["importance"]) if row["importance"] else 0.5
                                ),
                                "confidence": (
                                    float(row["confidence"]) if row["confidence"] else 1.0
                                ),
                                "source_type": str(row["source_type"]) + "-merged",
                                "agent_id": str(row.get("agent_id", "default")),
                                "user_id": (str(row["user_id"]) if row.get("user_id") else None),
                                "session_id": (
                                    str(row["session_id"]) if row.get("session_id") else None
                                ),
                                "metadata": json.dumps(metadata),
                                "now": datetime.now(UTC),
                                "cluster_id": f"merge-{int(time.time())}",
                                "similarity": dup["similarity"],
                            }

                            target_conn.execute(merge_query, params)
                            merged_count += 1

                else:  # skip strategy
                    skipped_count = len(duplicate_memories)

            execution_time_ms = (time.time() - start_time) * 1000

            return json.dumps(
                {
                    "status": "completed",
                    "total_source": len(source_memories_raw),
                    "duplicates_found": len(duplicate_memories),
                    "new_imported": imported_count,
                    "updated": updated_count,
                    "merged": merged_count,
                    "skipped": skipped_count,
                    "errors": 0,
                    "execution_time_ms": execution_time_ms,
                    "strategy": strategy,
                    "threshold": threshold,
                    "backup_path": str(backup_path) if backup_path else None,
                    "message": f"Successfully merged {imported_count} new memories from {source_path}",
                },
                indent=2,
            )

        except Exception as e:
            logger.error(f"Merge operation failed: {e}", exc_info=True)
            return json.dumps({"status": "error", "error": str(e)})

    async def _export_shared(
        self,
        min_age_days: int = 1,
        project_path: str | None = None,
    ) -> str:
        """
        Export delta memories to kuzu-memory-shared/ as JSON files.

        Args:
            min_age_days: Minimum age of memories to export (default 1 day).
            project_path: Override for the project root directory.

        Returns:
            JSON-formatted export results.
        """
        from .sharing import export_shared

        try:
            root = Path(project_path) if project_path else self.project_root
            db_path = self._get_db_path()
            if db_path.is_dir():
                db_path = db_path / "memories.db"
            summary = export_shared(
                db_path=db_path,
                project_root=root,
                min_age_days=min_age_days,
            )
            return json.dumps(
                {
                    "status": "completed",
                    **summary,
                    "message": (
                        f"Exported {summary['exported_count']} memories to "
                        f"{len(summary['files_written'])} file(s) in "
                        f"{root / 'kuzu-memory-shared'}"
                    ),
                },
                indent=2,
            )
        except (ImportError, RuntimeError) as e:
            return json.dumps({"status": "error", "error": str(e)})
        except Exception as e:
            logger.error("Export-shared failed: %s", e, exc_info=True)
            return json.dumps({"status": "error", "error": str(e)})

    async def _import_shared(
        self,
        project_path: str | None = None,
        dry_run: bool = False,
    ) -> str:
        """
        Import new memories from kuzu-memory-shared/ JSON files.

        Args:
            project_path: Override for the project root directory.
            dry_run: If True, preview without making changes.

        Returns:
            JSON-formatted import results.
        """
        from .sharing import import_shared

        try:
            root = Path(project_path) if project_path else self.project_root
            db_path = self._get_db_path()
            if db_path.is_dir():
                db_path = db_path / "memories.db"
            summary = import_shared(
                db_path=db_path,
                project_root=root,
                dry_run=dry_run,
            )
            verb = "would be imported" if dry_run else "imported"
            return json.dumps(
                {
                    "status": "completed",
                    **summary,
                    "message": (
                        f"{summary['imported_count']} memories {verb}, "
                        f"{summary['skipped_duplicates']} duplicates skipped."
                    ),
                },
                indent=2,
            )
        except (ImportError, RuntimeError) as e:
            return json.dumps({"status": "error", "error": str(e)})
        except Exception as e:
            logger.error("Import-shared failed: %s", e, exc_info=True)
            return json.dumps({"status": "error", "error": str(e)})

    async def run(self) -> None:
        """Run the MCP server with queue processor."""
        # Initialize queue processor for bash hooks
        from .queue_processor import HookQueueProcessor

        self.queue_processor = HookQueueProcessor()

        # Use stdin/stdout for MCP communication
        init_options = InitializationOptions(
            server_name="kuzu-memory",
            server_version=__version__,
            capabilities=self.server.get_capabilities(
                notification_options=NotificationOptions(),
                experimental_capabilities={},
            ),
        )

        # Use stdio_server async context manager for proper stream handling
        async with stdio_server() as (read_stream, write_stream):
            logger.info(f"KuzuMemory MCP Server running for project: {self.project_root}")

            # Start queue processor in background
            await self.queue_processor.start()

            try:
                # Run the MCP server with proper streams
                await self.server.run(
                    read_stream, write_stream, init_options, raise_exceptions=False
                )
            except asyncio.CancelledError:
                logger.info("Server shutdown requested")
                raise
            except GeneratorExit:
                logger.info("Server context manager cleanup")
                # Allow proper cleanup without ignoring GeneratorExit
                raise
            finally:
                # Stop queue processor on shutdown
                if self.queue_processor:
                    await self.queue_processor.stop()


async def main() -> None:
    """Main entry point for MCP server."""
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    )

    if not MCP_AVAILABLE:
        print(
            "Error: MCP SDK is not installed. Install with: pip install mcp",
            file=sys.stderr,
        )
        sys.exit(1)

    try:
        server = KuzuMemoryMCPServer()
        await server.run()
    except KeyboardInterrupt:
        logger.info("Server stopped by user")
    except asyncio.CancelledError:
        logger.info("Server cancelled")
    except GeneratorExit:
        logger.info("Server generator exit")
        # Don't suppress GeneratorExit - let it propagate for proper cleanup
        raise
    except Exception as e:
        logger.error(f"Server error: {e}")
        sys.exit(1)


# Alternative simplified server for environments without MCP SDK
class SimplifiedMCPServer:
    """
    Simplified MCP-like server that works without the MCP SDK.

    This provides a basic JSON-RPC interface for kuzu-memory operations.
    """

    def __init__(self, project_root: Path | None = None) -> None:
        """Initialize simplified server."""
        self.project_root = project_root or Path.cwd()

    async def handle_request(self, request: dict[str, Any]) -> dict[str, Any]:
        """Handle JSON-RPC style requests."""
        method = request.get("method", "")
        params = request.get("params", {})

        if method == "enhance":
            result = await self._run_cli_command(
                ["memory", "enhance", params.get("prompt", ""), "--format", "plain"]
            )
        elif method == "learn":
            result = await self._run_cli_command(
                ["memory", "learn", params.get("content", ""), "--quiet", "--no-wait"]
            )
        elif method == "recall":
            result = await self._run_cli_command(
                ["memory", "recall", params.get("query", ""), "--format", "json"]
            )
        elif method == "stats":
            result = await self._run_cli_command(["status", "--format", "json"])
        else:
            result = {"error": f"Unknown method: {method}"}

        return {"jsonrpc": "2.0", "id": request.get("id", 1), "result": result}

    async def _run_cli_command(self, args: list[str]) -> Any:
        """Run kuzu-memory CLI command."""
        try:
            cmd = ["kuzu-memory", *args]
            process = await asyncio.create_subprocess_exec(
                *cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
                cwd=self.project_root,
            )
            stdout, stderr = await process.communicate()

            if process.returncode == 0:
                output = stdout.decode().strip()
                # Try to parse as JSON
                try:
                    return json.loads(output)
                except json.JSONDecodeError:
                    return output
            else:
                return {"error": stderr.decode().strip()}
        except Exception as e:
            return {"error": str(e)}

    async def run_stdio(self) -> None:
        """Run server using stdio for communication."""
        reader = asyncio.StreamReader()
        protocol = asyncio.StreamReaderProtocol(reader)
        await asyncio.get_running_loop().connect_read_pipe(lambda: protocol, sys.stdin)

        try:
            while True:
                try:
                    line = await reader.readline()
                    if not line:
                        break

                    request = json.loads(line.decode())
                    response = await self.handle_request(request)

                    print(json.dumps(response))
                    sys.stdout.flush()
                except asyncio.CancelledError:
                    logger.info("Simplified server cancelled")
                    break
                except Exception as e:
                    logger.error(f"Error handling request: {e}")
        except GeneratorExit:
            logger.info("Simplified server generator exit")
            # Allow proper cleanup
            raise


if __name__ == "__main__":
    # Run the appropriate server based on availability
    if MCP_AVAILABLE:
        asyncio.run(main())
    else:
        # Fallback to simplified server
        logging.basicConfig(level=logging.INFO)
        server = SimplifiedMCPServer()
        asyncio.run(server.run_stdio())
