"""MCP Server for Mem-Brain API using FastMCP."""

import json
import logging
from typing import Any, Dict, List, Optional, Union
import httpx
from fastmcp import FastMCP
from fastmcp.server.context import request_ctx
from fastmcp.exceptions import ToolError
from starlette.requests import Request
from starlette.responses import JSONResponse

from mem_brain_mcp.client import APIClient
from mem_brain_mcp.config import settings
from mem_brain_mcp import __version__

# The comprehensive agent instructions (embedded for MCP distribution)
AGENT_INSTRUCTIONS = """You are an intelligent assistant with a persistent, evolving memory graph.

## 🎯 CORE DIRECTIVE
**Synthesize**, don't just retrieve. Connect user's request to their past preferences, habits, and constraints.

## 🔍 MEMORY WORKFLOW

**1. SEARCH FIRST & SMART** — Before answering personal questions, call `search_memories`.
   - **Formulate specific, natural language queries**, NOT simple keywords.
     - ❌ `query="maga"` (Weak)
     - ✅ `query="Who is Maga and what is his relationship to me?"` (Strong)
   - **Use `keyword_filter` for Scoping**: Deterministically isolate context by project, session, or topic.
     - ✅ `search_memories(query="...", keyword_filter="project-x")` (Matches memories tagged with project-x)
     - ✅ `search_memories(query="...", keyword_filter="session-.*-2026")` (Regex match for 2026 sessions)
   - Check `related_memories` field — these are auto-expanded graph neighbors.
   - Synthesize: If "coffee" result links to "acid reflux", suggest cold brew.

**2. PATTERN RECOGNITION** — Don't just echo memories back.
   - ❌ "I see a memory that says you like navy"
   - ✅ "This matches the navy aesthetic you've been leaning into"

**3. PASSIVE STORAGE** — When user reveals preferences, store the **FACT** (not conversation).
   - User: "I think I wanna try that sushi spot" → Store: "User interested in new sushi restaurant"

---

## 🛠️ TOOLS

| Tool | When to Use |
|------|-------------|
| `search_memories(query, k=5)` | Before answering ANY personal question |
| `get_memories(memory_ids)` | Need full details for specific IDs |
| `add_memory(content, tags=[], category="")` | User reveals preference/goal/fact |
| `get_stats()` | Check memory coverage and system health |
| `delete_memories(memory_id)` | Memory is wrong or user requests deletion |

---

## 📝 STORAGE GUIDELINES

**Write FACTS, not conversation:**
- ✅ "User prefers dark mode interfaces"
- ❌ "You said you like dark mode"

**Tagging patterns:**
- Domain: `health`, `work`, `finance`, `tech`, `food`, `travel`
- Type: `preference`, `constraint`, `goal`, `fact`, `event`
- Priority: `important`, `routine`, `temporary`

**System handles updates automatically:**
- When user changes preferences, just add the new memory
- The system automatically links related memories and manages updates
- No manual linking or unlinking needed

---

## 🔄 CHANGING PREFERENCES

| Signal | Action |
|--------|--------|
| "I'm trying X", "exploring Y" | ADD new memory (temporary exploration) |
| "I no longer like X", "I switched to Y" | ADD new memory (the system handles updates automatically) |
| Contradictory with equal weight | ADD with temporal context ("as of 2025") |

---

## ✅ BEST PRACTICES

| DO | DON'T |
|----|-------|
| Search before answering personal Q's | Guess without searching |
| Check `related_memories` field | Ignore graph connections |
| Store explicit facts | Store vague conversation |
| Synthesize across memories | Just list facts |

**Remember:** You're not a database. Connect the dots to provide thoughtful, personalized responses."""


def _get_request_token() -> Optional[str]:
    """Extract JWT token from request headers.

    Returns:
        JWT token string if found, None otherwise
    """
    try:
        ctx = request_ctx.get()
        if hasattr(ctx, "request") and hasattr(ctx.request, "headers"):
            headers = ctx.request.headers
            # Try Authorization Bearer token (primary method)
            auth_header = headers.get("authorization", "") or headers.get("Authorization", "")
            if auth_header.startswith("Bearer "):
                return auth_header[7:]
            # Fallback to X-API-Key header (for backward compatibility)
            api_key = headers.get("x-api-key") or headers.get("X-API-Key")
            if api_key:
                return api_key
    except Exception as e:
        logger.debug(f"Could not extract token from request: {e}")
    return None


logger = logging.getLogger(__name__)


async def _get_api_client() -> APIClient:
    """Get API client with per-request JWT token."""
    token = _get_request_token()
    if token:
        logger.debug(f"Using JWT token from request headers: {token[:20]}...")
        client = APIClient(api_key=token)  # api_key parameter now holds JWT token
        logger.debug(f"API client created with base_url: {client.base_url}")
        return client
    # Fallback to config API key (for single-user scenarios)
    if settings.api_key:
        logger.debug("Using config API key as fallback")
        logger.debug(f"API client (fallback) base_url: {api_client.base_url}")
        return api_client  # Global instance
    # No token available
    logger.error("No authentication token available - neither from headers nor config")
    raise ToolError(
        "No authentication token provided. Please login using the login tool or configure your JWT token in your MCP client headers."
    )


# Initialize FastMCP server
mcp = FastMCP("Mem-Brain MCP")

# Initialize API client
api_client = APIClient()


# ============================================================================
# RESOURCES (Documentation that LLMs can read)
# ============================================================================


@mcp.resource("mem-brain://docs/workflow-guide")
def workflow_guide() -> str:
    """Complete guide to the memory workflow: search strategies, pattern recognition, storage guidelines, and best practices."""
    return """# Memory Workflow Guide

## 🎯 CORE DIRECTIVE
**Synthesize**, don't just retrieve. Connect user's request to their past preferences, habits, and constraints.

## 🔍 MEMORY WORKFLOW

**1. SEARCH FIRST & SMART** — Before answering personal questions, call `search_memories`.
   - **Formulate specific, natural language queries**, NOT simple keywords.
     - ❌ `query="maga"` (Weak)
     - ✅ `query="Who is Maga and what is his relationship to me?"` (Strong)
   - Check `related_memories` field — these are auto-expanded graph neighbors.
   - Synthesize: If "coffee" result links to "acid reflux", suggest cold brew.

**2. PATTERN RECOGNITION** — Don't just echo memories back.
   - ❌ "I see a memory that says you like navy"
   - ✅ "This matches the navy aesthetic you've been leaning into"

**3. PASSIVE STORAGE** — When user reveals preferences, store the **FACT** (not conversation).
   - User: "I think I wanna try that sushi spot" → Store: "User interested in new sushi restaurant"

## ✅ BEST PRACTICES

| DO | DON'T |
|----|-------|
| Search before answering personal Q's | Guess without searching |
| Check `related_memories` field | Ignore graph connections |
| Store explicit facts | Store vague conversation |
| Synthesize across memories | Just list facts |

**Remember:** You're not a database. Connect the dots to provide thoughtful, personalized responses.
"""


@mcp.resource("mem-brain://docs/storage-guidelines")
def storage_guidelines() -> str:
    """Best practices for storing facts, tagging patterns, and avoiding duplicates."""
    return """# Storage Guidelines

## Write FACTS, not conversation

- ✅ "User prefers dark mode interfaces"
- ❌ "You said you like dark mode"

## Tagging Patterns

**Domains**: `health`, `work`, `finance`, `tech`, `food`, `travel`
**Types**: `preference`, `constraint`, `goal`, `fact`, `event`
**Priority**: `important`, `routine`, `temporary`

## System Handles Updates Automatically

- When user changes preferences, just add the new memory
- The system automatically links related memories and manages updates
- No manual linking or unlinking needed

## Changing Preferences

| Signal | Action |
|--------|--------|
| "I'm trying X", "exploring Y" | ADD new memory (temporary exploration) |
| "I no longer like X", "I switched to Y" | ADD new memory (the system handles updates automatically) |
| Contradictory with equal weight | ADD with temporal context ("as of 2025") |
"""


# ============================================================================
# TOOLS (Operations)
# ============================================================================


@mcp.tool()
async def get_agent_instructions() -> str:
    """Get comprehensive system prompt and best practices for using the memory system effectively. This contains the intelligence for smart memory management, search strategies, and agent workflows."""
    return AGENT_INSTRUCTIONS


@mcp.tool()
async def add_memory(
    content: str, tags: Optional[Union[List[str], str]] = None, category: Optional[str] = None
) -> str:
    """Create a new memory with content, optional tags, and category. Use this when user reveals preferences, goals, or facts. Store FACTS, not conversation. Examples: 'User prefers dark mode' vs 'You said you like dark mode'. See mem-brain://docs/storage-guidelines for tagging patterns. DO NOT store conversation snippets - only store factual information.

    IMPORTANT: Before creating a new memory, you MUST first search existing memories using search_memories() to check if a similar memory already exists. This prevents duplicates and helps maintain memory quality. Only create a new memory if no similar memory is found.

    Parameters:
        content (str, REQUIRED): The memory content to store. Must be a non-empty string.
            - Cannot be None, empty string, or whitespace-only
            - Example: "User prefers Python over JavaScript"
            - Example: "User prefers dark mode interfaces"

        tags (list[str] or str, optional): Tags to categorize the memory.
            - Can be None (default), a list of strings, a comma-separated string, or a JSON array string
            - If omitted, the system will auto-generate tags based on content
            - Example: ["coding", "preferences"]
            - Example: "coding,preferences" (comma-separated)
            - Example: '["coding", "preferences"]' (JSON string)
            - Note: The system auto-generates relevant tags, so providing tags is optional

        category (str, optional): Category name for the memory.
            - Can be None (default) or a non-empty string
            - Example: "interests"
            - Example: "preferences"

    Returns:
        str: A formatted string with the memory ID and details of the created memory.

    Common Errors and Solutions:
        - Error: "Tool call arguments for mcp were invalid"
          Solution: Ensure 'content' parameter is provided as a string. Example: add_memory(content="User prefers dark mode")

        - Error: "The 'content' parameter cannot be empty"
          Solution: Provide non-empty content. Example: add_memory(content="User loves Python programming")

        - Error: "tags must be a list"
          Solution: Pass tags as a list. Example: add_memory(content="...", tags=["coding"]) not tags="coding"

    Example workflow:
        1. search_memories(query="User prefers Python")  # Check for existing memories
        2. If no similar memory found, then: add_memory(content="User prefers Python over JavaScript", tags=["coding", "preferences"])

    Examples:
        # Basic usage (required parameter only)
        add_memory(content="User prefers dark mode")

        # With tags
        add_memory(content="User loves Python programming", tags=["coding", "preferences"])

        # With tags and category
        add_memory(
            content="User loves working with TypeScript",
            tags=["coding", "typescript"],
            category="interests"
        )

        # Tags as empty list (treated as None)
        add_memory(content="User prefers coffee", tags=[])
    """
    # Validate parameters with detailed error messages
    if content is None:
        raise ToolError(
            "The 'content' parameter is required but was not provided.\n"
            'Example: add_memory(content="User prefers dark mode")\n'
            'Example: add_memory(content="User loves Python programming", tags=["coding"])'
        )

    if not isinstance(content, str):
        raise ToolError(
            f"The 'content' parameter must be a string, but got {type(content).__name__}.\n"
            f"Received: {repr(content)}\n"
            'Example: add_memory(content="User prefers dark mode")'
        )

    content_str = str(content).strip()
    if not content_str:
        raise ToolError(
            "The 'content' parameter cannot be empty or whitespace-only.\n"
            "Please provide a non-empty string with actual content.\n"
            'Example: add_memory(content="User prefers dark mode")\n'
            'Example: add_memory(content="User loves Python programming")'
        )

    try:
        logger.info(
            f"add_memory called - content length: {len(content_str)}, tags: {tags}, category: {category}"
        )
        logger.debug(f"add_memory full content: {content_str[:100]}...")

        # Normalize tags: handle various input formats and convert to list of strings
        normalized_tags = None
        if tags is not None:
            if isinstance(tags, list):
                # Validate list contents are strings
                if tags:
                    invalid_items = [item for item in tags if not isinstance(item, str)]
                    if invalid_items:
                        raise ToolError(
                            f"The 'tags' parameter must be a list of strings, but found non-string items: {invalid_items}\n"
                            f'Example: add_memory(content="...", tags=["coding", "preferences"])\n'
                            f'Example: add_memory(content="...", tags=["personal", "pets"])'
                        )
                normalized_tags = tags if tags else None  # Empty list becomes None
            elif isinstance(tags, str):
                tags_str = tags.strip()
                if not tags_str:
                    normalized_tags = None
                else:
                    # Try to parse as JSON array first (e.g., '["tag1", "tag2"]')
                    try:
                        parsed = json.loads(tags_str)
                        if isinstance(parsed, list):
                            normalized_tags = [
                                str(item).strip() for item in parsed if str(item).strip()
                            ]
                        else:
                            # If JSON but not a list, treat as single tag
                            normalized_tags = [tags_str]
                    except (json.JSONDecodeError, ValueError):
                        # Not JSON, try comma-separated string
                        if "," in tags_str:
                            normalized_tags = [
                                tag.strip() for tag in tags_str.split(",") if tag.strip()
                            ]
                        else:
                            # Single tag string
                            normalized_tags = [tags_str]
            else:
                raise ToolError(
                    f"The 'tags' parameter must be a list of strings, a comma-separated string, or None, but got {type(tags).__name__}.\n"
                    f"Received: {repr(tags)}\n"
                    'Example: add_memory(content="...", tags=["coding", "preferences"])\n'
                    'Example: add_memory(content="...", tags="coding,preferences")\n'
                    'Example: add_memory(content="...", tags=None)  # or omit tags parameter'
                )

        # Normalize category: convert empty string to None
        normalized_category = (
            category.strip()
            if category and isinstance(category, str) and category.strip()
            else None
        )

        client = await _get_api_client()
        logger.debug(
            f"Calling API client.add_memory with content='{content_str[:50]}...', tags={normalized_tags}, category={normalized_category}"
        )

        result = await client.add_memory(content_str, normalized_tags, normalized_category)

        logger.info(f"Memory created successfully: {result.get('memory_id', 'unknown')}")
        memory = result.get("memory")
        if memory:
            return f"Memory created: {result.get('memory_id', 'unknown')}\n{_format_memory(memory)}"
        return f"Memory created: {result.get('memory_id', 'unknown')}"
    except httpx.HTTPStatusError as e:
        error_detail = e.response.text if e.response else "Unknown error"
        logger.error(f"API error: {e.response.status_code} - {error_detail}")
        if e.response.status_code == 401:
            raise ToolError(
                "Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers."
            )
        elif e.response.status_code == 400:
            raise ToolError(f"Invalid request: {error_detail}")
        raise ToolError(f"Failed to create memory: HTTP {e.response.status_code} - {error_detail}")
    except ToolError:
        raise
    except Exception as e:
        logger.error(f"Unexpected error in add_memory: {e}", exc_info=True)
        raise ToolError(f"Error creating memory: {str(e)}")


@mcp.tool()
async def search_memories(
    query: str, k: int = 5, keyword_filter: Optional[Union[str, List[str]]] = None
) -> str:
    """Search memories using semantic similarity with optional regex-based tag filtering.

    CRITICAL: Formulate specific, natural language queries, NOT simple keywords.
    Examples: ✅ 'Who is Maga and what is their relationship to me?' vs ❌ 'maga'.

    The keyword_filter allows for deterministic scoping (e.g., project-specific or session-specific).
    - String: Match memories where ANY tag matches this regex pattern (case-insensitive).
    - List[str]: Match memories where ALL patterns in the list satisfy at least one tag (AND logic).

    Parameters:
        query (str, REQUIRED): Search query string. Use natural language questions, not keywords.
            - Example: "Who is Rakshith and what did he build?"
            - Example: "What are the user's preferences for programming languages?"

        k (int, optional): Number of results to return. Default is 5.
            - Must be between 1 and 100

        keyword_filter (str | list[str], optional): Case-insensitive regex pattern or list of patterns to filter by tags.
            - Example: "session-v1" (String: returns only memories tagged with session-v1)
            - Example: "project-.*-2026" (Regex string: matches any 2026 project tag)
            - Example: ["work", "important"] (List: returns only memories tagged with BOTH work AND important)

    Returns:
        str: Formatted search results with memory nodes and relationship edges.

    Common Errors and Solutions:
        - Error: "Query cannot be empty"
          Solution: Provide a non-empty search query. Example: search_memories(query="What is the user's name?")

        - Error: "k must be between 1 and 100"
          Solution: Provide k between 1 and 100. Example: search_memories(query="...", k=10)

    Examples:
        # Scoped search for a specific session
        search_memories(query="What progress was made?", keyword_filter="session-v1")

        # Scoped search using regex
        search_memories(query="Database decisions", keyword_filter="project-.*")
    """
    # Validate parameters with detailed error messages
    if query is None:
        raise ToolError(
            "The 'query' parameter is required but was not provided.\n"
            'Example: search_memories(query="Who is Rakshith?")\n'
            'Example: search_memories(query="What are the user\'s preferences?")'
        )

    if not isinstance(query, str):
        raise ToolError(
            f"The 'query' parameter must be a string, but got {type(query).__name__}.\n"
            f"Received: {repr(query)}\n"
            'Example: search_memories(query="Who is Rakshith?")'
        )

    query_str = query.strip()
    if not query_str:
        raise ToolError(
            "The 'query' parameter cannot be empty or whitespace-only.\n"
            "Provide a natural language question or search query.\n"
            'Example: search_memories(query="Who is Rakshith?")\n'
            'Example: search_memories(query="What are the user\'s preferences?")'
        )

    if not isinstance(k, int):
        raise ToolError(
            f"The 'k' parameter must be an integer, but got {type(k).__name__}.\n"
            f"Received: {repr(k)}\n"
            'Example: search_memories(query="...", k=10)'
        )

    if not (1 <= k <= 100):
        raise ToolError(
            f"The 'k' parameter must be between 1 and 100, but got {k}.\n"
            'Example: search_memories(query="...", k=10)'
        )

    try:
        logger.info(
            f"search_memories called - query: '{query_str[:50]}...', k: {k}, filter: {keyword_filter}"
        )
        client = await _get_api_client()
        result = await client.search_memories(query_str, k, keyword_filter)
        return f"Found {result.get('count', 0)} results:\n{_format_search_results(result.get('results', []))}"
    except httpx.HTTPStatusError as e:
        error_detail = e.response.text if e.response else "Unknown error"
        logger.error(f"API error: {e.response.status_code} - {error_detail}")
        if e.response.status_code == 401:
            raise ToolError(
                "Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers."
            )
        raise ToolError(
            f"Failed to search memories: HTTP {e.response.status_code} - {error_detail}"
        )
    except ToolError:
        raise
    except Exception as e:
        logger.error(f"Unexpected error in search_memories: {e}", exc_info=True)
        raise ToolError(f"Error searching memories: {str(e)}")


@mcp.tool()
async def get_memories(memory_ids: List[str]) -> str:
    """Retrieve one or more memories by ID. Use this when you need full details for specific memories identified from search results.

    Parameters:
        memory_ids (list[str], REQUIRED): List of memory IDs to retrieve. Must be a non-empty list.
            - Example: ["480c1f76-bcdf-4491-8781-24510db992e3"]
            - Example: ["480c1f76-...", "300d9716-...", "6fb6b23f-..."]
            - Get memory IDs from search_memories() results

    Returns:
        str: Formatted details of the retrieved memories.

    Common Errors and Solutions:
        - Error: "memory_ids cannot be empty"
          Solution: Provide a list with at least one memory ID. Example: get_memories(memory_ids=["480c1f76-..."])

        - Error: "Memory IDs cannot be empty"
          Solution: Ensure all IDs in the list are non-empty strings. Example: get_memories(memory_ids=["480c1f76-..."])

        - Error: "memory_ids must be a list"
          Solution: Pass memory_ids as a list. Example: get_memories(memory_ids=["..."]) not memory_ids="..."

    Examples:
        # Get single memory
        get_memories(memory_ids=["480c1f76-bcdf-4491-8781-24510db992e3"])

        # Get multiple memories
        get_memories(memory_ids=["480c1f76-...", "300d9716-...", "6fb6b23f-..."])
    """
    # Validate parameters with detailed error messages
    if memory_ids is None:
        raise ToolError(
            "The 'memory_ids' parameter is required but was not provided.\n"
            'Example: get_memories(memory_ids=["480c1f76-bcdf-4491-8781-24510db992e3"])\n'
            'Example: get_memories(memory_ids=["480c1f76-...", "300d9716-..."])'
        )

    if not isinstance(memory_ids, list):
        raise ToolError(
            f"The 'memory_ids' parameter must be a list of strings, but got {type(memory_ids).__name__}.\n"
            f"Received: {repr(memory_ids)}\n"
            'Example: get_memories(memory_ids=["480c1f76-..."])'
        )

    if not memory_ids:
        raise ToolError(
            "The 'memory_ids' parameter cannot be an empty list.\n"
            "Provide at least one memory ID.\n"
            'Example: get_memories(memory_ids=["480c1f76-bcdf-4491-8781-24510db992e3"])'
        )

    # Validate each memory ID in the list
    validated_ids = []
    for i, memory_id in enumerate(memory_ids):
        if memory_id is None:
            raise ToolError(
                f"Memory ID at index {i} is None. All memory IDs must be non-empty strings.\n"
                'Example: get_memories(memory_ids=["480c1f76-..."])'
            )
        if not isinstance(memory_id, str):
            raise ToolError(
                f"Memory ID at index {i} must be a string, but got {type(memory_id).__name__}.\n"
                f"Received: {repr(memory_id)}\n"
                'Example: get_memories(memory_ids=["480c1f76-..."])'
            )
        memory_id_str = memory_id.strip()
        if not memory_id_str:
            raise ToolError(
                f"Memory ID at index {i} cannot be empty or whitespace-only.\n"
                "Get memory IDs from search_memories() or get_memories() results.\n"
                'Example: get_memories(memory_ids=["480c1f76-bcdf-4491-8781-24510db992e3"])'
            )
        validated_ids.append(memory_id_str)

    try:
        logger.info(f"get_memories called - count: {len(validated_ids)}")
        client = await _get_api_client()
        result = await client.get_memories(validated_ids)
        memories = result.get("memories", [])
        return f"Retrieved {len(memories)} memories:\n{_format_memories_list(memories)}"
    except httpx.HTTPStatusError as e:
        error_detail = e.response.text if e.response else "Unknown error"
        logger.error(f"API error: {e.response.status_code} - {error_detail}")
        if e.response.status_code == 401:
            raise ToolError(
                "Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers."
            )
        elif e.response.status_code == 404:
            raise ToolError(
                f"One or more memories not found.\nVerify the memory IDs are correct by searching for them first."
            )
        raise ToolError(f"Failed to get memories: HTTP {e.response.status_code} - {error_detail}")
    except ToolError:
        raise
    except Exception as e:
        logger.error(f"Unexpected error in get_memories: {e}", exc_info=True)
        raise ToolError(f"Error getting memories: {str(e)}")


@mcp.tool()
async def delete_memories(
    memory_id: Optional[str] = None, tags: Optional[str] = None, category: Optional[str] = None
) -> str:
    """Delete memories by ID or by filter (tags/category). If memory_id is provided, it takes precedence over filters. Use for removing wrong memories or when user explicitly requests deletion.

    Parameters:
        memory_id (str, optional): Specific memory ID to delete. Takes precedence over filters.
            - Example: "480c1f76-bcdf-4491-8781-24510db992e3"
            - Get memory IDs from search_memories() or get_memories() results

        tags (str, optional): Comma-separated tags for filter-based deletion.
            - Example: "coding,preferences"
            - Example: "personal,pets"
            - Only used if memory_id is not provided

        category (str, optional): Category name for filter-based deletion.
            - Example: "interests"
            - Example: "preferences"
            - Only used if memory_id is not provided

    Returns:
        str: A message indicating how many memories were deleted and their IDs.

    Common Errors and Solutions:
        - Error: "At least one parameter must be provided"
          Solution: Provide memory_id, tags, or category. Example: delete_memories(memory_id="...")

        - Error: "memory_id cannot be empty"
          Solution: Provide a valid memory ID or omit the parameter. Example: delete_memories(memory_id="480c1f76-...")

    Examples:
        # Delete by memory ID
        delete_memories(memory_id="480c1f76-bcdf-4491-8781-24510db992e3")

        # Delete by tags
        delete_memories(tags="coding,preferences")

        # Delete by category
        delete_memories(category="interests")
    """
    # Validate that at least one parameter is provided
    if memory_id is None and tags is None and category is None:
        raise ToolError(
            "At least one parameter (memory_id, tags, or category) must be provided to delete memories.\n"
            'Example: delete_memories(memory_id="480c1f76-bcdf-4491-8781-24510db992e3")\n'
            'Example: delete_memories(tags="coding,preferences")\n'
            'Example: delete_memories(category="interests")'
        )

    # Validate memory_id if provided
    if memory_id is not None:
        if not isinstance(memory_id, str):
            raise ToolError(
                f"The 'memory_id' parameter must be a string or None, but got {type(memory_id).__name__}.\n"
                f"Received: {repr(memory_id)}\n"
                'Example: delete_memories(memory_id="480c1f76-bcdf-4491-8781-24510db992e3")'
            )
        memory_id_str = memory_id.strip()
        if not memory_id_str:
            raise ToolError(
                "The 'memory_id' parameter cannot be empty or whitespace-only.\n"
                "Get memory IDs from search_memories() or get_memories() results.\n"
                'Example: delete_memories(memory_id="480c1f76-bcdf-4491-8781-24510db992e3")'
            )
    else:
        memory_id_str = None

    # Validate tags if provided
    if tags is not None:
        if not isinstance(tags, str):
            raise ToolError(
                f"The 'tags' parameter must be a string or None, but got {type(tags).__name__}.\n"
                f"Received: {repr(tags)}\n"
                'Example: delete_memories(tags="coding,preferences")'
            )
        tags_str = tags.strip()
        if not tags_str:
            raise ToolError(
                "The 'tags' parameter cannot be empty or whitespace-only.\n"
                "Provide comma-separated tags or omit the parameter.\n"
                'Example: delete_memories(tags="coding,preferences")'
            )
    else:
        tags_str = None

    # Validate category if provided
    if category is not None:
        if not isinstance(category, str):
            raise ToolError(
                f"The 'category' parameter must be a string or None, but got {type(category).__name__}.\n"
                f"Received: {repr(category)}\n"
                'Example: delete_memories(category="interests")'
            )
        category_str = category.strip()
        if not category_str:
            raise ToolError(
                "The 'category' parameter cannot be empty or whitespace-only.\n"
                "Provide a category name or omit the parameter.\n"
                'Example: delete_memories(category="interests")'
            )
    else:
        category_str = None

    try:
        logger.info(
            f"delete_memories called - memory_id: {memory_id_str}, tags: {tags_str}, category: {category_str}"
        )
        client = await _get_api_client()
        result = await client.delete_memories(memory_id_str, tags_str, category_str)
        deleted_ids = result.get("memory_ids", [])[:10]
        return f"Deleted {result.get('deleted_count', 0)} memories. IDs: {', '.join(deleted_ids)}"
    except httpx.HTTPStatusError as e:
        error_detail = e.response.text if e.response else "Unknown error"
        logger.error(f"API error: {e.response.status_code} - {error_detail}")
        if e.response.status_code == 401:
            raise ToolError(
                "Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers."
            )
        elif e.response.status_code == 404:
            raise ToolError(
                f"Memory not found: {memory_id_str}\nVerify the memory_id is correct by searching for it first."
            )
        raise ToolError(
            f"Failed to delete memories: HTTP {e.response.status_code} - {error_detail}"
        )
    except ToolError:
        raise
    except Exception as e:
        logger.error(f"Unexpected error in delete_memories: {e}", exc_info=True)
        raise ToolError(f"Error deleting memories: {str(e)}")


@mcp.tool()
async def get_stats() -> str:
    """Get comprehensive statistics about the memory system.

    **Use this tool to:**
    - Check how many memories you have stored
    - See total link count and link density
    - View top tags and memory distribution
    - Understand your knowledge graph structure

    **Why this matters:**
    - Provides context about memory coverage
    - Shows if the system is actively used
    - Reveals patterns in what you store
    - Helps identify gaps or over-representation

    Returns formatted statistics including:
    - Total memories count
    - Total links count
    - Average links per memory
    - Top tags by frequency
    - Memory categories/tags distribution

    **Example:**
    get_stats()
    """
    try:
        logger.info("get_stats called - retrieving memory system statistics")
        client = await _get_api_client()
        result = await client.get_stats()

        # Extract statistics
        total_memories = result.get("total_memories", 0)
        total_links = result.get("total_links", 0)
        avg_links = result.get("avg_links_per_memory", 0)
        top_tags = result.get("top_tags", [])

        # Format response
        lines = [
            "📊 Memory System Statistics",
            "",
            f"Total Memories: {total_memories}",
            f"Total Links: {total_links}",
            f"Average Links per Memory: {avg_links:.2f}",
        ]

        # Add top tags if available
        if top_tags:
            lines.append("")
            lines.append("Top Tags:")
            for tag_data in top_tags[:10]:  # Show top 10
                if isinstance(tag_data, dict):
                    tag_name = tag_data.get("tag", "unknown")
                    count = tag_data.get("count", 0)
                    lines.append(f"  - {tag_name}: {count} memories")
                else:
                    lines.append(f"  - {tag_data}")

        return "\n".join(lines)
    except httpx.HTTPStatusError as e:
        error_detail = e.response.text if e.response else "Unknown error"
        logger.error(f"API error: {e.response.status_code} - {error_detail}")
        if e.response.status_code == 401:
            raise ToolError(
                "Authentication failed. Please login using the 'login' tool or configure your JWT token in the MCP client headers."
            )
        raise ToolError(f"Failed to get statistics: HTTP {e.response.status_code} - {error_detail}")
    except ToolError:
        raise
    except Exception as e:
        logger.error(f"Unexpected error in get_stats: {e}", exc_info=True)
        raise ToolError(f"Error getting statistics: {str(e)}")


# ============================================================================
# HELPER FUNCTIONS
# ============================================================================


def _format_memory(memory: Optional[Dict[str, Any]]) -> str:
    """Format a single memory for display."""
    if not memory:
        return "Memory data not available"

    lines = [
        f"ID: {memory.get('id', 'unknown')}",
        f"Content: {memory.get('content', '')[:200]}",
        f"Tags: {', '.join(memory.get('tags', []))}",
        f"Context: {memory.get('context', 'N/A')}",
        f"Links: {len(memory.get('links', []))} connections",
    ]

    # Add evolution history if available
    evolution_history = memory.get("evolution_history", [])
    if evolution_history:
        lines.append(f"Evolution History: {len(evolution_history)} version(s)")
        # Show current version first
        current_content = memory.get("content", "")
        lines.append(f"  Current Version: {current_content}")
        lines.append("")
        # Show historical versions (oldest to newest)
        for i, entry in enumerate(evolution_history, 1):
            if entry.get("type") == "content_update":
                old_content = entry.get("old_content", "")
                timestamp = entry.get("timestamp", "unknown")
                lines.append(f"  Version {i} ({timestamp}): {old_content}")
            elif entry.get("type") == "evolution":
                old_context = entry.get("old_context", "N/A")
                new_context = entry.get("new_context", "N/A")
                timestamp = entry.get("timestamp", "unknown")
                lines.append(
                    f"  Evolution {i} ({timestamp}): Context '{old_context}' → '{new_context}'"
                )

    return "\n".join(lines)


def _format_memories_list(memories: List[Dict[str, Any]]) -> str:
    """Format a list of memories for display."""
    if not memories:
        return "No memories found"

    formatted = []
    for i, mem in enumerate(memories, 1):
        formatted.append(f"{i}. {_format_memory(mem)}")
    return "\n\n".join(formatted)


def _format_search_results(results: List[Dict[str, Any]]) -> str:
    """Format search results for display."""
    if not results:
        return "No results found"

    formatted = []
    for i, result in enumerate(results, 1):
        if result.get("type") == "memory_node":
            formatted.append(f"{i}. Memory Node (score: {result.get('semantic_score', 0):.3f})")
            formatted.append(f"   {_format_memory(result)}")
            related = result.get("related_memories", [])
            if related:
                formatted.append(f"   Related: {len(related)} memories")
        elif result.get("type") == "relationship_edge":
            formatted.append(f"{i}. Relationship Edge (score: {result.get('score', 0):.3f})")
            source = result.get("source", {})
            target = result.get("target", {})

            # Show source node data
            formatted.append(f"   Source Node:")
            formatted.append(f"     ID: {source.get('id', 'unknown')}")
            formatted.append(f"     Content: {source.get('content', 'N/A')}")
            if source.get("context") and source.get("context") != "General":
                formatted.append(f"     Context: {source.get('context', 'N/A')}")
            if source.get("tags"):
                formatted.append(f"     Tags: {', '.join(source.get('tags', []))}")
            if source.get("keywords"):
                formatted.append(f"     Keywords: {', '.join(source.get('keywords', []))}")

            # Show target node data
            formatted.append(f"   Target Node:")
            formatted.append(f"     ID: {target.get('id', 'unknown')}")
            formatted.append(f"     Content: {target.get('content', 'N/A')}")
            if target.get("context") and target.get("context") != "General":
                formatted.append(f"     Context: {target.get('context', 'N/A')}")
            if target.get("tags"):
                formatted.append(f"     Tags: {', '.join(target.get('tags', []))}")
            if target.get("keywords"):
                formatted.append(f"     Keywords: {', '.join(target.get('keywords', []))}")
        formatted.append("")

    return "\n".join(formatted)


# Add health check endpoint for load balancers
@mcp.custom_route("/health", methods=["GET"])
async def health_check(request: Request) -> JSONResponse:
    """Health check endpoint for load balancers."""
    return JSONResponse(content={"status": "healthy", "service": "mem-brain-mcp"}, status_code=200)


def _mask_api_url(url: str) -> str:
    """Mask the API URL, showing only the first 1/4 and hiding the rest."""
    if not url:
        return "Not set"
    # Show first 1/4 of the URL, mask the rest
    url_length = len(url)
    visible_length = max(1, url_length // 4)
    if visible_length >= url_length:
        return url
    visible_part = url[:visible_length]
    masked_part = "*" * (url_length - visible_length)
    return f"{visible_part}{masked_part}"


def run_server():
    """Run the FastMCP server with HTTP transport."""
    logger.info(
        f"Starting Mem-Brain MCP Server v{__version__} on {settings.mcp_server_host}:{settings.mcp_server_port}"
    )
    logger.info(f"API URL: {_mask_api_url(settings.api_url)}")
    logger.info(f"API Key: {'***' if settings.api_key else 'Not set'}")

    # Configure CORS for browser-based and MCP clients
    from starlette.middleware import Middleware
    from starlette.middleware.cors import CORSMiddleware

    middleware = [
        Middleware(
            CORSMiddleware,
            allow_origins=["*"],  # Allow all origins for MCP clients
            allow_methods=["GET", "POST", "DELETE", "OPTIONS"],
            allow_headers=[
                "mcp-protocol-version",
                "mcp-session-id",
                "Authorization",
                "Content-Type",
                "Accept",
            ],
            expose_headers=["mcp-session-id"],
        )
    ]

    # Use http_app with CORS middleware and run with uvicorn
    # Note: http_app() handles the /mcp path automatically
    app = mcp.http_app(middleware=middleware, path="/mcp")

    # Import uvicorn (should be available via FastMCP dependencies)
    try:
        import uvicorn
    except ImportError:
        # Fallback: use mcp.run if uvicorn not available
        logger.warning("uvicorn not available, using mcp.run() without CORS")
        mcp.run(
            transport="http",
            host=settings.mcp_server_host,
            port=settings.mcp_server_port,
            path="/mcp",
        )
        return

    uvicorn.run(
        app,
        host=settings.mcp_server_host,
        port=settings.mcp_server_port,
    )
