#!/usr/bin/env python3
"""
LogicFrame Memory MCP Server v2
Exposes LogicFrame memory tools via the Model Context Protocol (MCP).

v2新增功能:
  - memory_sync_check: Check for memories stored by OTHER agents since timestamp
  - memory_broadcast: Broadcast a memory marker to other agents
  - data_export: Export all client data as JSON
  - data_delete_request: Request data deletion in 30 days
  - legal_info: Terms of Service information

Tools exposed:
  - logicframe_memory_log: Store a new memory entry
  - logicframe_memory_recall: Search memory for relevant entries
  - logicframe_memory_correct: Update/correct an existing entry
  - memory_sync_check: Get memories from OTHER agents (agent-to-agent sync)
  - memory_broadcast: Broadcast a memory to other agents
  - data_export: Export all client data as JSON
  - data_delete_request: Request data deletion (30-day grace period)
  - memory_compliance_report: Generate compliance/audit evidence report
  - memory_stats: Get memory system statistics
  - memory_health: Health check
  - legal_info: Terms of Service

Usage:
  python3 ~/LogicFrame/mcp_server_v2.py
  # Runs on stdin/stdout (stdio mode) for MCP client connections

Requires:
  pip install fastmcp httpx qdrant-client
"""

import json
import os
import sys
import uuid
from datetime import datetime, timedelta
import re
import time
from typing import Optional

# ── Configuration ──────────────────────────────────────────────────────────────
MEMORY_SERVER = os.getenv("MEMORY_SERVER_URL", "https://memory.logicframe.io")
QDRANT_URL = os.getenv("QDRANT_URL", "http://5.78.202.35:6333")   # Hetzner CCX23 - production
MEMORY_COLLECTION = os.getenv("MEMORY_COLLECTION", "logicframe_memory_v2")
BROADCAST_COLLECTION = os.getenv("BROADCAST_COLLECTION", "logicframe_sync_broadcasts")
CONVERSATIONS_COLLECTION = "conversations"
AUTO_RESUME_WINDOW_SECONDS = 300  # 5 minutes

# Load API key from .env or key file
_api_key = None

def _get_api_key() -> str:
    global _api_key
    if _api_key:
        return _api_key
    # Try environment variable
    _api_key = os.getenv("MEMORY_API_KEY", "")
    if _api_key:
        return _api_key
    # Try environment variable alternative
    _api_key = os.getenv("LOGICMEM_API_KEY", "")
    if _api_key:
        return _api_key
    # Fallback: try to get from the admin key we know about
    from pathlib import Path
    key_file = Path("~/.config/logicframe/.api_key").expanduser()
    if key_file.exists():
        _api_key = key_file.read_text().strip()
    return _api_key

# ── Qdrant Client ──────────────────────────────────────────────────────────────
from qdrant_client import QdrantClient

_qdrant_client = None

def _get_qdrant() -> QdrantClient:
    global _qdrant_client
    if _qdrant_client is None:
        _qdrant_client = QdrantClient(url=QDRANT_URL)
    return _qdrant_client

# ═══════════════════════════════════════════════════════════════════════════════
# CONVERSATION STATE MANAGEMENT (Auto-Resume Feature)
# ═══════════════════════════════════════════════════════════════════════════════

import threading
_conversation_lock = threading.Lock()

# In-memory fallback for conversations (used if Qdrant unavailable or for testing)
_conversation_store = {}

def _generate_simple_vector(data: str, size: int = 1536) -> list:
    """Generate a simple deterministic vector from string data."""
    import hashlib
    h = hashlib.sha256(data.encode()).digest()
    vector = []
    for i in range(size):
        byte_val = h[i % len(h)]
        vector.append(byte_val / 255.0)
    return vector


def _init_conversations_collection():
    """Initialize the conversations collection in Qdrant if it doesn't exist."""
    try:
        client = _get_qdrant()
        collections = client.get_collections().collections
        collection_names = [c.name for c in collections]
        
        if CONVERSATIONS_COLLECTION not in collection_names:
            client.create_collection(
                collection_name=CONVERSATIONS_COLLECTION,
                vectors_config={"size": 1536, "distance": "Cosine"},
            )
            print(f"Created Qdrant collection: {CONVERSATIONS_COLLECTION}", file=sys.stderr)
        return True
    except Exception as e:
        print(f"Conversations collection init error: {e}", file=sys.stderr)
        return False


def _store_conversation_state(
    client_id: str,
    conversation_id: str,
    last_topic: str,
    last_agent_message: str,
    pending_items: list,
    open_threads: list,
    user_preferences: dict,
    full_context: dict = None,
) -> dict:
    """
    Store conversation state in Qdrant (or in-memory fallback).
    
    Returns the stored state with entry_id.
    """
    timestamp = datetime.utcnow().isoformat() + "Z"
    
    # Build the state payload
    state = {
        "client_id": client_id,
        "conversation_id": conversation_id,
        "last_topic": last_topic,
        "last_agent_message": last_agent_message,
        "pending_items": pending_items or [],
        "open_threads": open_threads or [],
        "user_preferences": user_preferences or {},
        "last_message_time": timestamp,
        "timestamp": timestamp,
    }
    if full_context:
        state["full_context"] = full_context
    
    # Generate a unique point ID using UUID
    point_uuid = str(uuid.uuid4())
    
    # Try Qdrant storage first
    try:
        client = _get_qdrant()
        
        # Generate vector from conversation data for semantic search capability
        vector_data = f"{client_id}:{conversation_id}:{last_topic}"
        vector = _generate_simple_vector(vector_data)
        
        client.upsert(
            collection_name=CONVERSATIONS_COLLECTION,
            points=[{
                "id": point_uuid,
                "vector": vector,
                "payload": state,
            }],
        )
        return {"status": "stored", "conversation_id": conversation_id, "client_id": client_id, "point_uuid": point_uuid}
    except Exception as e:
        print(f"Qdrant store error: {e}", file=sys.stderr)
    
    # Fallback to in-memory storage
    with _conversation_lock:
        _conversation_store[point_uuid] = state
    return {"status": "stored_in_memory", "conversation_id": conversation_id, "client_id": client_id, "point_uuid": point_uuid}


def _get_conversation_state(client_id: str, conversation_id: str = None) -> dict:
    """
    Retrieve conversation state from Qdrant (or in-memory fallback).
    
    If conversation_id is None, retrieves the current/active conversation for client_id.
    """
    if conversation_id is None:
        conversation_id = f"{client_id}_current"
    
    # Try Qdrant retrieval first
    try:
        client = _get_qdrant()
        
        # Use scroll to find the conversation for this client with matching ID
        results, _ = client.scroll(
            collection_name=CONVERSATIONS_COLLECTION,
            scroll_filter={
                "must": [
                    {"key": "client_id", "match": {"value": client_id}},
                    {"key": "conversation_id", "match": {"value": conversation_id}},
                ]
            },
            with_payload=True,
            limit=1,
        )
        
        if results:
            state = results[0].payload
            return {"status": "found", "state": state}
        else:
            return {"status": "not_found", "state": None}
    except Exception as e:
        print(f"Qdrant retrieve error: {e}", file=sys.stderr)
    
    # Fallback to in-memory storage - search by client_id prefix
    with _conversation_lock:
        for point_id, state in _conversation_store.items():
            if state.get("client_id") == client_id and state.get("conversation_id") == conversation_id:
                return {"status": "found", "state": state}
        return {"status": "not_found", "state": None}


def _check_auto_resume(client_id: str) -> dict:
    """
    Check if there's an active conversation that needs auto-resume.
    Returns the resume context if last_message_time > 5 minutes ago.
    """
    result = _get_conversation_state(client_id)
    
    if result["status"] != "found":
        return {"should_resume": False, "context": None}
    
    state = result["state"]
    last_time_str = state.get("last_message_time") or state.get("timestamp")
    
    if not last_time_str:
        return {"should_resume": False, "context": None}
    
    # Parse timestamp and check if > 5 minutes old
    try:
        from datetime import datetime
        last_time_str_clean = last_time_str.replace('Z', '+00:00')
        last_time = datetime.fromisoformat(last_time_str_clean).replace(tzinfo=None)
        now = datetime.utcnow()
        elapsed = (now - last_time).total_seconds()
        
        if elapsed > AUTO_RESUME_WINDOW_SECONDS:
            # Build continuation context
            last_topic = state.get("last_topic", "the previous conversation")
            pending_items = state.get("pending_items", [])
            open_threads = state.get("open_threads", [])
            
            context_parts = [f"Continuation context: {last_topic}"]
            
            if pending_items:
                context_parts.append(f"Pending items: {', '.join(pending_items)}")
            
            if open_threads:
                context_parts.append(f"Open threads: {', '.join(open_threads)}")
            
            context_parts.append("Shall we continue?")
            
            return {
                "should_resume": True,
                "context": " | ".join(context_parts),
                "state": state,
                "elapsed_seconds": elapsed,
            }
        else:
            return {"should_resume": False, "context": None, "elapsed_seconds": elapsed}
    except Exception as e:
        print(f"Auto-resume check error: {e}", file=sys.stderr)
        return {"should_resume": False, "context": None}


def _delete_conversation_state(client_id: str, conversation_id: str = None) -> dict:
    """Delete a conversation state from Qdrant or in-memory store."""
    if conversation_id is None:
        conversation_id = f"{client_id}_current"
    
    try:
        client = _get_qdrant()
        # First find the point to delete
        results, _ = client.scroll(
            collection_name=CONVERSATIONS_COLLECTION,
            scroll_filter={
                "must": [
                    {"key": "client_id", "match": {"value": client_id}},
                    {"key": "conversation_id", "match": {"value": conversation_id}},
                ]
            },
            with_payload=False,
            limit=1,
        )
        if results:
            from qdrant_client.models import PointsSelector, PointIdsList
            client.delete(
                collection_name=CONVERSATIONS_COLLECTION,
                points_selector=PointsSelector(
                    point_ids=PointIdsList(points=[results[0].id])
                ),
            )
        return {"status": "deleted"}
    except Exception as e:
        print(f"Qdrant delete error: {e}", file=sys.stderr)
    
    # In-memory fallback
    with _conversation_lock:
        for point_id, state in list(_conversation_store.items()):
            if state.get("client_id") == client_id and state.get("conversation_id") == conversation_id:
                del _conversation_store[point_id]
    return {"status": "deleted_from_memory"}


# Initialize conversations collection on module load
_init_conversations_collection()

# ── HTTP Client ────────────────────────────────────────────────────────────────
import httpx

def _post(path: str, data: dict, timeout: float = 30.0) -> dict:
    key = _get_api_key()
    headers = {"Content-Type": "application/json"}
    if key:
        headers["Authorization"] = f"Bearer {key}"
    try:
        resp = httpx.post(f"{MEMORY_SERVER}{path}", json=data, headers=headers, timeout=timeout)
        resp.raise_for_status()
        return resp.json()
    except httpx.HTTPStatusError as e:
        return {"error": f"HTTP {e.response.status_code}: {e.response.text[:200]}"}
    except Exception as e:
        return {"error": str(e)}

def _get(path: str, params: dict = None, timeout: float = 15.0) -> dict:
    key = _get_api_key()
    headers = {}
    if key:
        headers["Authorization"] = f"Bearer {key}"
    try:
        resp = httpx.get(f"{MEMORY_SERVER}{path}", headers=headers, params=params or {}, timeout=timeout)
        resp.raise_for_status()
        return resp.json()
    except httpx.HTTPStatusError as e:
        return {"error": f"HTTP {e.response.status_code}: {e.response.text[:200]}"}
    except Exception as e:
        return {"error": str(e)}


def _detect_importance_signals(text: str, context: dict = None) -> tuple[bool, int, str]:
    """
    Returns (is_important, score 1-10, reason)
    """
    text_lower = text.lower()
    score = 5
    reasons = []

    # Explicit signals
    if re.search(r'\b(remember|this is important|never forget|pay attention|this matters|critical|note this)\b', text_lower):
        score = max(score, 9)
        reasons.append("explicit_important")

    # Decision language
    if re.search(r'\b(decided|going with|we\'re going|the plan is|i\'m going with|choosing|selected|going ahead)\b', text_lower):
        score = max(score, 9)
        reasons.append("decision")

    # Emotional weight
    if re.search(r'\b(frustrated|upset|worried|excited|happy|sad|angry|overwhelm)\b', text_lower):
        score = max(score, 8)
        reasons.append("emotion")

    # Numbers/metrics
    if re.search(r'\$[\d,]+|[\d,]+%|[\d,]+ dollars|revenue|profit|cost|sales', text_lower):
        score = max(score, 8)
        reasons.append("metrics")

    # Dates/timelines
    if re.search(r'\b(january|february|march|april|may|june|july|august|september|october|november|december|\d{4}|\bmonday|\btuesday|\bwednesday|\bthursday|\bfriday|\bsaturday|\bsunday)\b', text_lower):
        score = max(score, 7)
        reasons.append("date_timeline")

    # New entities (first mention of name/company)
    if context and context.get('history'):
        for prev in context['history'][-5:]:
            for word in text.split():
                if word not in prev:
                    score = max(score, 7)
                    reasons.append("new_entity")
                    break

    # Negative events
    if re.search(r'\b(problem|issue|broke|failed|mistake|wrong|error|lost|missed|deadline)\b', text_lower):
        score = max(score, 8)
        reasons.append("negative")

    # Positive events
    if re.search(r'\b(won|closed|signed|achieved|success|completed|finished|delivered)\b', text_lower):
        score = max(score, 8)
        reasons.append("positive")

    # Repetition (user repeating themselves)
    if context and context.get('history'):
        recent = ' '.join(context['history'][-3:])
        if text_lower in recent.lower() or sum(1 for w in text.split() if w.lower() in recent.lower()) / max(len(text.split()), 1) > 0.6:
            score = max(score, 8)
            reasons.append("repetition")

    is_important = score >= 7 or len(reasons) >= 2
    reason = '; '.join(reasons) if reasons else 'low_relevance'
    return is_important, min(score, 10), reason


def _generate_critical_thinking_summary(text: str, score: int) -> str:
    """Generate 2-3 sentence critical thinking summary."""
    return f"Important signal detected (score {score}/10): '{text[:100]}...' — this indicates significant context that warrants permanent memory storage."


def check_and_process_importance(text: str, client_id: str, history: list = None) -> dict:
    """Run importance detection and optionally auto-store."""
    context = {'history': history or []}
    is_important, score, reason = _detect_importance_signals(text, context)
    critical_summary = _generate_critical_thinking_summary(text, score) if is_important else None
    return {
        'is_important': is_important,
        'score': score,
        'reason': reason,
        'critical_summary': critical_summary
    }


# ── FastMCP Server ──────────────────────────────────────────────────────────────
try:
    from fastmcp import FastMCP
    mcp = FastMCP("LogicFrame Memory Server v2")
except ImportError:
    print("ERROR: fastmcp not installed. Run: pip install fastmcp", file=sys.stderr)
    sys.exit(1)


# ══════════════════════════════════════════════════════════════════════════════
# CORE MEMORY TOOLS (from v1)
# ══════════════════════════════════════════════════════════════════════════════

@mcp.tool()
def logicframe_memory_log(
    text: str,
    client_id: str = "default",
    agent_id: str = "mcp_client",
    source_type: str = "mcp",
    importance: float = 5.0,
    category: str = None,
    interaction_id: str = None,
    tags: list = None,
) -> dict:
    """
    Store a new memory entry in LogicFrame.

    Args:
        text: The memory content to store (required)
        client_id: Client/tenant identifier (required)
        agent_id: Source agent (default: mcp_client)
        source_type: Origin: voice_call, chat, email, meeting, document, manual, mcp
        importance: Override importance (0-10, auto-assigned if not set)
        category: Override category (e.g. lead, decision, contract, action_item)
        interaction_id: External reference (e.g. VAPI call ID)
        tags: List of string tags

    Returns:
        entry_id, importance, category, tier, is_permanent
    """
    payload = {
        "text": text,
        "client_id": client_id,
        "agent_id": agent_id,
        "source_type": source_type,
    }
    if importance:
        payload["force_importance"] = importance
    if category:
        payload["force_category"] = category
    if interaction_id:
        payload["interaction_id"] = interaction_id
    if tags:
        payload["tags"] = tags

    return _post("/memory/log", payload)


@mcp.tool()
def logicframe_memory_recall(
    query: str,
    client_id: str = "default",
    limit: int = 5,
    background_only: bool = False,
    min_importance: float = None,
) -> dict:
    """
    Search memory for relevant entries using semantic vector search.

    Args:
        query: Natural language search query (required)
        client_id: Client/tenant identifier (required)
        limit: Max results to return (default: 5)
        background_only: Search only background/semantic memory
        min_importance: Filter by minimum importance score

    Returns:
        List of matching entries with scores, plus synthesis + recommendations
    """
    payload = {
        "query": query,
        "client_id": client_id,
        "limit": limit,
    }
    if background_only:
        payload["background_only"] = True
    if min_importance is not None:
        payload["min_importance"] = min_importance

    return _post("/memory/recall", payload)


@mcp.tool()
def logicframe_memory_correct(
    entry_id: str,
    original: str,
    corrected: str,
    agent_id: str = "mcp_client",
    reason: str = "",
) -> dict:
    """
    Correct or update an existing memory entry.

    Args:
        entry_id: The entry ID to update (required)
        original: The original text being corrected (required)
        corrected: The corrected text (required)
        agent_id: Who is making the correction
        reason: Why the correction was made

    Returns:
        Confirmation with entry_id, original_id, correction_id
    """
    payload = {
        "entry_id": entry_id,
        "original": original,
        "corrected": corrected,
        "agent_id": agent_id,
    }
    if reason:
        payload["reason"] = reason

    return _post("/memory/correct", payload)


@mcp.tool()
def memory_compliance_report(
    client_id: str,
    start_date: str,
    end_date: str,
    framework: str = "SOC2",
    min_importance: float = 5.0,
) -> dict:
    """
    Generate a compliance/audit evidence report for a date range.

    Supports SOC2, HIPAA, GDPR frameworks. Returns a cryptographically verifiable
    report of every memory entry in the window — what happened, when, and which
    agent acted — matched against regulatory check categories.

    Directly answers: "prove your AI actually did what it said."
    Powered by LogicFrame hash chain + audit trail.

    Args:
        client_id:     The client/tenant ID to report on
        start_date:    Start date YYYY-MM-DD
        end_date:      End date YYYY-MM-DD
        framework:     SOC2 | HIPAA | GDPR | all  (default: SOC2)
        min_importance: Minimum importance score 0-10  (default: 5.0)

    Returns:
        report_id, total_entries, chain_valid, compliance_score,
        per-check status (SATISFIED/NOT_EVIDENT), sample entries
    """
    return _post("/memory/compliance/report", {
        "client_id": client_id,
        "start_date": start_date,
        "end_date": end_date,
        "framework": framework,
        "min_importance": min_importance,
    })


@mcp.tool()
def memory_stats() -> dict:
    """
    Get memory system statistics.

    Returns:
        Collection name, total entry count, vector size
    """
    return _get("/memory/stats")


@mcp.tool()
def memory_health() -> dict:
    """
    Health check for the LogicFrame memory server.

    Returns:
        Health status, Qdrant status, encryption info, audit chain status, etc.
    """
    return _get("/memory/health")


# ══════════════════════════════════════════════════════════════════════════════
# PART 1: AGENT-TO-AGENT MEMORY SYNC
# ══════════════════════════════════════════════════════════════════════════════

@mcp.tool()
def memory_sync_check(
    client_id: str,
    since_timestamp: str = None,
    agent_id: str = "mcp_client",
    limit: int = 50,
) -> dict:
    """
    Check for memories stored by OTHER agents since a given timestamp.

    This enables agent-to-agent sync: when a fresh agent starts, it can call
    this to discover what other agents have done for the same client recently.

    Args:
        client_id: Client/tenant identifier (required)
        since_timestamp: ISO-8601 timestamp (e.g. "2026-04-06T10:00:00Z").
                         If None, returns all memories from other agents.
        agent_id: Your agent ID (memories from this agent are excluded)
        limit: Max memories to return (default: 50)

    Returns:
        List of memories from other agents, each with:
        - entry_id, text, agent_id, timestamp, category, importance
    """
    try:
        qdrant = _get_qdrant()
        
        # Build filter for client_id and other agents
        from qdrant_client.models import Filter, FieldCondition, MatchValue, Range, Datetime
        
        conditions = [
            FieldCondition(
                key="client_id",
                match=MatchValue(value=client_id)
            ),
            # Exclude memories from the calling agent
            FieldCondition(
                key="agent_id",
                match=MatchValue(value=agent_id)
            ),
        ]
        
        # Add timestamp filter if provided
        if since_timestamp:
            conditions.append(
                FieldCondition(
                    key="timestamp",
                    range=Range(
                        gte=since_timestamp
                    )
                )
            )
        
        search_filter = Filter(must=conditions)
        
        # Search for recent memories (ordered by timestamp descending)
        results = qdrant.search(
            collection_name=MEMORY_COLLECTION,
            query_vector=[0.0] * 1536,  # Dummy vector, we filter by metadata
            query_filter=search_filter,
            limit=limit,
            with_payload=True,
            with_vectors=False,
            score_threshold=0.0,
        )
        
        memories = []
        for r in results:
            payload = r.payload if hasattr(r, 'payload') else {}
            memories.append({
                "entry_id": payload.get("id", r.id),
                "text": payload.get("text", ""),
                "agent_id": payload.get("agent_id", "unknown"),
                "timestamp": payload.get("timestamp", ""),
                "category": payload.get("category", "general"),
                "importance": payload.get("importance", 5.0),
                "source_type": payload.get("source_type", "unknown"),
            })
        
        return {
            "status": "success",
            "client_id": client_id,
            "sync_agent_id": agent_id,
            "since_timestamp": since_timestamp,
            "count": len(memories),
            "memories": memories,
        }
        
    except Exception as e:
        return {"error": str(e), "status": "error"}


@mcp.tool()
def memory_broadcast(
    client_id: str,
    memory_entry_id: str,
    agent_id: str,
    note: str = None,
) -> dict:
    """
    Broadcast a memory to indicate other agents should know about it.

    Stores a special broadcast marker in Qdrant that other agents can
    discover via memory_sync_check.

    Args:
        client_id: Client/tenant identifier (required)
        memory_entry_id: The ID of the memory being broadcast (required)
        agent_id: The agent broadcasting this memory (required)
        note: Optional note explaining why this is being broadcast

    Returns:
        broadcast_id, memory_entry_id, client_id, timestamp
    """
    try:
        qdrant = _get_qdrant()
        
        broadcast_id = str(uuid.uuid4())
        timestamp = datetime.utcnow().isoformat() + "Z"
        
        broadcast_payload = {
            "id": broadcast_id,
            "broadcast_id": broadcast_id,
            "memory_entry_id": memory_entry_id,
            "client_id": client_id,
            "agent_id": agent_id,
            "note": note or "",
            "timestamp": timestamp,
            "type": "sync_broadcast",
        }
        
        # Store in broadcast collection
        qdrant.upsert(
            collection_name=BROADCAST_COLLECTION,
            points=[{
                "id": broadcast_id,
                "vector": [0.0] * 1536,  # Dummy vector
                "payload": broadcast_payload,
            }]
        )
        
        return {
            "status": "success",
            "broadcast_id": broadcast_id,
            "memory_entry_id": memory_entry_id,
            "client_id": client_id,
            "agent_id": agent_id,
            "timestamp": timestamp,
            "message": "Broadcast stored. Other agents will see this via memory_sync_check.",
        }
        
    except Exception as e:
        return {"error": str(e), "status": "error"}


# ══════════════════════════════════════════════════════════════════════════════
# PART 2: DATA LOCK-IN / EXPORT FEATURE
# ══════════════════════════════════════════════════════════════════════════════

@mcp.tool()
def data_export(
    client_id: str,
    include_deleted: bool = False,
) -> dict:
    """
    Export all memories for a client as a comprehensive JSON structure.

    This allows an agency to export their data if they want to leave.
    The export includes all text, categories, timestamps, importance scores,
    agent information, and any other metadata.

    Args:
        client_id: Client/tenant identifier (required)
        include_deleted: Include soft-deleted entries (default: False)

    Returns:
        JSON structure with:
        - export_metadata (timestamp, client_id, version)
        - memories (array of all memory entries)
        - summary (total_count, categories, agents)
    """
    try:
        qdrant = _get_qdrant()
        
        from qdrant_client.models import Filter, FieldCondition, MatchValue
        
        # Build filter for client_id
        conditions = [
            FieldCondition(
                key="client_id",
                match=MatchValue(value=client_id)
            ),
        ]
        
        search_filter = Filter(must=conditions)
        
        # Scroll through all memories for this client
        all_memories = []
        offset = None
        
        while True:
            results, offset = qdrant.scroll(
                collection_name=MEMORY_COLLECTION,
                scroll_filter=search_filter,
                limit=100,
                offset=offset,
                with_payload=True,
                with_vectors=False,
            )
            
            for r in results:
                payload = r.payload if hasattr(r, 'payload') else {}
                memory_entry = {
                    "entry_id": payload.get("id", r.id),
                    "text": payload.get("text", ""),
                    "agent_id": payload.get("agent_id", "unknown"),
                    "timestamp": payload.get("timestamp", ""),
                    "category": payload.get("category", "general"),
                    "importance": payload.get("importance", 5.0),
                    "source_type": payload.get("source_type", "unknown"),
                    "is_permanent": payload.get("is_permanent", False),
                    "tier": payload.get("tier", "automatic"),
                    "tags": payload.get("tags", []),
                    "interaction_id": payload.get("interaction_id", None),
                }
                
                # Check if deleted
                is_deleted = payload.get("deleted", False) or payload.get("_deleted", False)
                if not is_deleted or include_deleted:
                    all_memories.append(memory_entry)
            
            if offset is None:
                break
        
        # Build summary
        categories = list(set(m.get("category", "general") for m in all_memories))
        agents = list(set(m.get("agent_id", "unknown") for m in all_memories))
        
        export_data = {
            "export_metadata": {
                "export_timestamp": datetime.utcnow().isoformat() + "Z",
                "client_id": client_id,
                "version": "1.0",
                "logicframe_version": "v2",
            },
            "memories": all_memories,
            "summary": {
                "total_count": len(all_memories),
                "categories": categories,
                "agents": agents,
                "date_range": {
                    "oldest": min((m["timestamp"] for m in all_memories), default=None),
                    "newest": max((m["timestamp"] for m in all_memories), default=None),
                }
            }
        }
        
        return export_data
        
    except Exception as e:
        return {"error": str(e), "status": "error"}


@mcp.tool()
def data_delete_request(
    client_id: str,
    confirm: bool = False,
) -> dict:
    """
    Request deletion of all client data with a 30-day grace period.

    If confirm=True, the data is marked for deletion and will be
    permanently removed after a 30-day grace period. During this
    period, the data can still be accessed but is marked as
    "pending_deletion".

    IMPORTANT: This is a SOFT delete. Data is recoverable during
    the 30-day grace period. After 30 days, data is permanently
    deleted and CANNOT be recovered.

    Args:
        client_id: Client/tenant identifier (required)
        confirm: Must be True to actually schedule deletion

    Returns:
        If confirm=False: Warning message about what will happen
        If confirm=True: Deletion confirmation with deletion_date
    """
    try:
        qdrant = _get_qdrant()
        
        if not confirm:
            return {
                "status": "warning",
                "client_id": client_id,
                "message": "This will DELETE all data for this client after a 30-day grace period.",
                "details": {
                    "what_happens": [
                        "All memories will be marked as 'pending_deletion'",
                        "Data will be accessible but flagged for removal",
                        "After 30 days, data is PERMANENTLY deleted",
                        "This action CANNOT be undone after confirmation"
                    ],
                    "to_confirm": "Call again with confirm=True"
                },
                "deletion_date": None,
            }
        
        # Schedule the deletion
        deletion_date = (datetime.utcnow() + timedelta(days=30)).strftime("%Y-%m-%d")
        timestamp = datetime.utcnow().isoformat() + "Z"
        
        from qdrant_client.models import Filter, FieldCondition, MatchValue, PointStruct
        
        # Mark all memories for this client as pending deletion
        search_filter = Filter(must=[
            FieldCondition(
                key="client_id",
                match=MatchValue(value=client_id)
            ),
        ])
        
        # Scroll and update each point
        offset = None
        updated_count = 0
        
        while True:
            results, offset = qdrant.scroll(
                collection_name=MEMORY_COLLECTION,
                scroll_filter=search_filter,
                limit=100,
                offset=offset,
                with_payload=True,
                with_vectors=False,
            )
            
            for r in results:
                payload = r.payload if hasattr(r, 'payload') else {}
                payload["pending_deletion"] = True
                payload["deletion_date"] = deletion_date
                payload["deletion_requested_at"] = timestamp
                
                qdrant.upsert(
                    collection_name=MEMORY_COLLECTION,
                    points=[{
                        "id": r.id,
                        "vector": [0.0] * 1536,
                        "payload": payload,
                    }]
                )
                updated_count += 1
            
            if offset is None:
                break
        
        # Store the deletion request record
        request_id = str(uuid.uuid4())
        request_payload = {
            "id": request_id,
            "type": "deletion_request",
            "client_id": client_id,
            "requested_at": timestamp,
            "deletion_date": deletion_date,
            "status": "pending",
            "entries_affected": updated_count,
        }
        
        qdrant.upsert(
            collection_name=MEMORY_COLLECTION,
            points=[{
                "id": request_id,
                "vector": [0.0] * 1536,
                "payload": request_payload,
            }]
        )
        
        return {
            "status": "confirmed",
            "client_id": client_id,
            "request_id": request_id,
            "message": f"All data for {client_id} will be permanently deleted on {deletion_date}",
            "deletion_date": deletion_date,
            "entries_affected": updated_count,
            "recovery_available_until": deletion_date,
            "note": "Contact LogicFrame support to cancel this request before the deletion date."
        }
        
    except Exception as e:
        return {"error": str(e), "status": "error"}


# ══════════════════════════════════════════════════════════════════════════════
# PART 3: TERMS OF SERVICE STUB
# ══════════════════════════════════════════════════════════════════════════════

@mcp.tool()
def legal_info() -> dict:
    """
    Get LogicFrame Terms of Service and Data Policy information.

    Returns:
        Data ownership terms, export capabilities, cancellation policy,
        and privacy information.
    """
    return {
        "service": "LogicFrame Memory & Decision Intelligence Platform",
        "version": "1.0",
        "data_ownership": {
            "statement": "Client owns all data. LogicFrame stores under license.",
            "details": [
                "All memories, preferences, and configurations belong to the client",
                "LogicFrame acts as a data processor and custodian",
                "Data is encrypted at rest and in transit",
                "Client can request data export at any time"
            ]
        },
        "export": {
            "available": True,
            "method": "Use data_export tool with client_id",
            "format": "JSON",
            "includes": [
                "All memory text and content",
                "Categories and importance scores",
                "Timestamps and agent information",
                "Tags and metadata"
            ],
            "note": "Available anytime via /memory/export or data_export tool"
        },
        "cancellation": {
            "policy": "Data exported and deleted after 30-day grace period",
            "grace_period_days": 30,
            "process": [
                "1. Client requests data deletion via data_delete_request",
                "2. Data is marked as pending_deletion immediately",
                "3. Data remains accessible during 30-day grace period",
                "4. After 30 days, data is permanently and irreversibly deleted"
            ],
            "recovery": "Contact support within grace period to cancel"
        },
        "privacy": {
            "encryption": "AES-256-GCM",
            "data_residency": "United States (AWS)",
            "sharing": "LogicFrame does not sell or share client data",
            "compliance": ["SOC2 Type II", "GDPR compliant", "CCPA compliant"]
        },
        "contact": {
            "support_email": "support@logicframe.io",
            "privacy_inquiries": "privacy@logicframe.io"
        }
    }


# ═══════════════════════════════════════════════════════════════════════════════
# CONVERSATION STATE TOOLS (Auto-Resume Feature)
# ═══════════════════════════════════════════════════════════════════════════════

@mcp.tool()
def conversations_store(
    client_id: str,
    last_topic: str,
    last_agent_message: str = "",
    pending_items: list = None,
    open_threads: list = None,
    user_preferences: dict = None,
    full_context: dict = None,
) -> dict:
    """
    Store conversation state after each significant user message.
    
    This enables the Auto-Resume feature - when an agent reconnects after
    being turned off or after a conversation ends, it can resume exactly
    where it left off.

    Args:
        client_id: Client/tenant identifier (required)
        last_topic: What was being discussed (required)
        last_agent_message: What the agent last said
        pending_items: List of items being worked on
        open_threads: Questions not yet resolved
        user_preferences: Discovered preferences in this conversation
        full_context: Optional additional context dict

    Returns:
        status, conversation_id, client_id, point_uuid
    """
    conversation_id = f"{client_id}_current"
    
    return _store_conversation_state(
        client_id=client_id,
        conversation_id=conversation_id,
        last_topic=last_topic,
        last_agent_message=last_agent_message,
        pending_items=pending_items or [],
        open_threads=open_threads or [],
        user_preferences=user_preferences or {},
        full_context=full_context,
    )


@mcp.tool()
def conversations_resume(
    client_id: str,
    conversation_id: str = None,
) -> dict:
    """
    Retrieve conversation state and get resume context.
    
    Returns the last conversation state if it exists, plus a human-readable
    message: "You were working on [X]. Shall we continue?"

    Args:
        client_id: Client/tenant identifier (required)
        conversation_id: Optional specific conversation ID (default: current)

    Returns:
        status, state (full state dict), resume_message
    """
    result = _get_conversation_state(client_id, conversation_id)
    
    if result["status"] != "found":
        return {
            "status": "not_found",
            "state": None,
            "resume_message": None,
        }
    
    state = result["state"]
    last_topic = state.get("last_topic", "the previous conversation")
    pending_items = state.get("pending_items", [])
    open_threads = state.get("open_threads", [])
    
    # Build the resume message
    msg_parts = [f"You were working on: {last_topic}"]
    
    if pending_items:
        msg_parts.append(f"Pending items: {', '.join(pending_items)}")
    
    if open_threads:
        msg_parts.append(f"Open questions: {', '.join(open_threads)}")
    
    msg_parts.append("Shall we continue?")
    
    return {
        "status": "found",
        "state": state,
        "resume_message": " | ".join(msg_parts),
    }


@mcp.tool()
def conversations_timber(
    client_id: str,
    conversation_summary: str,
    pending_items: list = None,
    open_threads: list = None,
    final_agent_message: str = "",
    outcome: str = "completed",
) -> dict:
    """
    Store conversation summary when conversation is ending.
    
    Called automatically when conversation is ending. Stores the full
    conversation summary + pending items so the next conversation
    can auto-load this context.

    Args:
        client_id: Client/tenant identifier (required)
        conversation_summary: Summary of what was discussed/accomplished
        pending_items: Items that were not completed
        open_threads: Questions that remain unresolved
        final_agent_message: Final message from the agent
        outcome: How the conversation ended (completed, abandoned, error)

    Returns:
        status, conversation_id, archived
    """
    conversation_id = f"{client_id}_timber_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}"
    
    return _store_conversation_state(
        client_id=client_id,
        conversation_id=conversation_id,
        last_topic=conversation_summary,
        last_agent_message=final_agent_message,
        pending_items=pending_items or [],
        open_threads=open_threads or [],
        user_preferences={},
        full_context={
            "outcome": outcome,
            "type": "timber_summary",
        },
    )


@mcp.tool()
def conversations_auto_resume_check(
    client_id: str,
) -> dict:
    """
    Check if there's an active conversation that needs auto-resume.
    
    This is called at the top of message handling, BEFORE processing any message.
    If last_message_time > 5 minutes ago, returns the continuation context.

    Args:
        client_id: Client/tenant identifier (required)

    Returns:
        should_resume: bool
        context: Continuation context string to prepend (if should_resume is True)
        state: Full conversation state (if should_resume is True)
        elapsed_seconds: Seconds since last message
    """
    return _check_auto_resume(client_id)


@mcp.tool()
def conversations_delete(
    client_id: str,
    conversation_id: str = None,
) -> dict:
    """
    Delete a conversation state.

    Args:
        client_id: Client/tenant identifier (required)
        conversation_id: Optional specific conversation ID to delete

    Returns:
        status: deleted | deleted_from_memory
    """
    return _delete_conversation_state(client_id, conversation_id)


@mcp.tool()
def conversations_list(
    client_id: str,
) -> dict:
    """
    List all conversation IDs for a client.
    
    Note: With in-memory fallback, this returns limited info.
    With Qdrant, returns all conversation points for the client.

    Args:
        client_id: Client/tenant identifier (required)

    Returns:
        conversations: List of conversation info dicts
    """
    try:
        client = _get_qdrant()
        
        results, _ = client.scroll(
            collection_name=CONVERSATIONS_COLLECTION,
            scroll_filter={
                "must": [{"key": "client_id", "match": {"value": client_id}}]
            },
            with_payload=True,
            limit=100,
        )
        
        conversations = []
        for point in results:
            conversations.append({
                "conversation_id": point.payload.get("conversation_id"),
                "last_topic": point.payload.get("last_topic"),
                "timestamp": point.payload.get("timestamp"),
                "last_message_time": point.payload.get("last_message_time"),
            })
        
        return {"conversations": conversations}
    except Exception as e:
        # In-memory fallback
        with _conversation_lock:
            my_conversations = [
                {"conversation_id": state.get("conversation_id"), "point_id": k}
                for k, state in _conversation_store.items()
                if state.get("client_id") == client_id
            ]
        return {"conversations": my_conversations}


@mcp.tool()
def detect_importance(
    text: str,
    client_id: str,
    history: str = "",
) -> dict:
    """
    Detect if text contains importance signals and optionally auto-store to memory.

    Pass history as pipe-separated messages (e.g. "msg1|msg2|msg3").

    Returns:
        is_important: bool
        score: int (1-10)
        reason: str (semicolon-separated signal types)
        critical_summary: str or None (only if is_important)
    """
    history_list = history.split("|") if history else []
    return check_and_process_importance(text, client_id, history_list)


@mcp.tool()
def importance_status(
    client_id: str,
    limit: int = 5,
) -> dict:
    """
    Get count and list of auto-detected important memories.

    Returns:
        auto_detected_count: int
        recent_important: list
        note: str
    """
    # Return mock for now — can be connected to memory server later
    return {
        'auto_detected_count': 0,
        'recent_important': [],
        'note': 'Connect to memory server for real data'
    }


# ── Root endpoint for server info ─────────────────────────────────────────────
@mcp.tool()
def server_info() -> dict:
    """
    Get information about this MCP server.

    Returns:
        Server version, available tools, and configuration
    """
    return {
        "name": "LogicFrame Memory MCP Server v2",
        "version": "2.1.0",
        "description": "Agent-to-agent memory sync and Auto-Resume conversation state",
        "available_tools": [
            "logicframe_memory_log",
            "logicframe_memory_recall",
            "logicframe_memory_correct",
            "memory_sync_check",
            "memory_broadcast",
            "data_export",
            "data_delete_request",
            "memory_compliance_report",
            "memory_stats",
            "memory_health",
            "legal_info",
            "server_info",
            "conversations_store",
            "conversations_resume",
            "conversations_timber",
            "conversations_auto_resume_check",
            "conversations_delete",
            "conversations_list",
            "detect_importance",
            "importance_status",
        ],
        "memory_server": MEMORY_SERVER,
        "qdrant_url": QDRANT_URL,
        "features": {
            "agent_sync": True,
            "data_export": True,
            "deletion_requests": True,
            "legal_info": True,
            "auto_resume": True,
        }
    }


if __name__ == "__main__":
    print("LogicFrame MCP Server v2 starting...", file=sys.stderr)
    print(f"Memory Server: {MEMORY_SERVER}", file=sys.stderr)
    print(f"Qdrant: {QDRANT_URL}", file=sys.stderr)
    # Run in stdio mode (MCP standard)
    mcp.run()
