"""
Memory Consolidation Plugin for Cheshire Cat

Phase 2: Sleep Consolidation Implementation

Implements human-like memory consolidation:
1. During the day: Store almost everything temporarily
2. On demand (or scheduled): Analyze conversations, keep important, delete trivial
3. Extract facts for declarative memory (per-user)

This mimics how human brains consolidate memories during REM sleep.
"""

from cat.mad_hatter.decorators import hook, tool
from datetime import datetime
import json
import os
from typing import List, Dict, Any
import re

print("\U0001f319 [Consolidation Plugin] Loading...")

# ===================================================================
# HYBRID TRIVIAL-MESSAGE CLASSIFIER
# ===================================================================
# Tiered approach:
#   DEFINITELY_TRIVIAL  → delete immediately (no LLM)
#   DEFINITELY_IMPORTANT → keep immediately (no LLM)  
#   BORDERLINE          → batch-send to LLM for classification
#
# Real-time filtering (discord_bridge) uses a subset of these heuristics
# without LLM. Consolidation runs the full hybrid pipeline.

# Tier 1: Messages that are ALWAYS trivial — exact string match only
DEFINITELY_TRIVIAL = frozenset([
    # Pure reactions
    'lol', 'lmao', 'haha', 'hehe', 'xd', 'rofl', 'lmfao', 'heh',
    # Acknowledgments
    'k', 'ok', 'okay', 'kk', 'yep', 'nope', 'yeah', 'nah',
    'cool', 'nice', 'neat', 'wow',
    'ty', 'thx', 'np', 'yw', 'gg', 'gj', 'wp', 'gz',
    # AFK/status
    'brb', 'gtg', 'afk', 'ttyl',
    # Acronyms that don't carry content alone
    'idk', 'tbh', 'imo', 'imho', 'omg', 'wtf', 'btw', 'nvm', 'jk', 'ikr', 'smh',
    'fyi', 'lmk',
    # Greetings/farewells (single word only)
    'hi', 'hey', 'hello', 'bye', 'cya', 'gn', 'gm', 'yo', 'sup',
    # Modern slang trash
    'based', 'true', 'real', 'same', 'facts',
])

# Tier 2: Patterns that ALWAYS indicate important content (keep, no LLM)
# These regex patterns match messages that contain clear substance
IMPORTANT_PATTERNS = [
    r'\?',                          # Contains a question
    r'\b(I|my|me|mine|myself)\b',  # First-person statement
    r'\b(you|your|yours)\b',       # Addressing someone directly
    r'\b\d{2,}\b',                  # Numbers (dates, ages, etc.)
    r'https?://',                   # Links
    r'<@\d+>',                      # Discord user mention
    r'<#\d+>',                      # Discord channel mention
]

def _classify_message_tier(content, metadata):
    """
    Classify a message into DEFINITELY_TRIVIAL, DEFINITELY_IMPORTANT, or BORDERLINE.
    
    Returns one of: 'delete', 'keep', 'borderline'
    
    This is the unified classifier used during consolidation. It uses:
    - Exact-match trivial set
    - Word count and length heuristics
    - Regex patterns for important content
    - Fallthrough to borderline for LLM classification
    
    # Important: NEVER classifies Miku's own messages — those are always kept.
    """
    text = content.strip()
    
    # Miku's own messages are always kept (speaker check)
    if metadata.get('speaker') == 'miku' or text.startswith('[Miku]:'):
        return 'keep'
    
    # Strip [User]: prefix (added by discord_bridge at storage time) so the
    # classifier analyzes the actual message content, not the label
    if text.startswith('[User]:'):
        text = text[len('[User]:'):].strip()
    
    text_lower = text.lower()
    word_count = len(text_lower.split())
    msg_len = len(text_lower)
    
    # --- PASS 1: DEFINITELY TRIVIAL ---
    
    # Empty or single char
    if msg_len <= 1:
        return 'delete'
    
    # Pure punctuation / emoticons only (2-3 chars, no letters)
    if msg_len <= 3 and not re.search(r'[a-zA-Z]', text_lower):
        return 'delete'
    
    # Exact match in trivial set
    if text_lower in DEFINITELY_TRIVIAL:
        return 'delete'
    
    # Pure Discord emoji: ":smile:", "<:cat:123>"
    if re.match(r'^:[\w_]+:$', text_lower) or re.match(r'^<a?:[\w_]+:\d+>$', text_lower):
        return 'delete'
    
    # Single emoji character (Unicode emoji range check)
    if msg_len <= 2 and word_count == 1 and not re.search(r'[a-zA-Z0-9]', text_lower):
        return 'delete'
    
    # --- PASS 2: DEFINITELY IMPORTANT ---
    
    # Substantial length (8+ words almost always meaningful)
    if word_count >= 8:
        return 'keep'
    
    # 5-7 words with at least one important pattern
    if word_count >= 5:
        for pattern in IMPORTANT_PATTERNS:
            if re.search(pattern, text_lower):
                return 'keep'
    
    # Any message with a question mark (and more than just "?")
    if '?' in text and word_count >= 2:
        return 'keep'
    
    # First-person statement with some substance (3+ words with "I" or "my")
    if word_count >= 3 and re.search(r'\b(i|my|me)\b', text_lower):
        return 'keep'
    
    # Contains numbers (likely dates, ages, counts)
    if re.search(r'\b\d{2,}\b', text_lower) and word_count >= 2:
        return 'keep'
    
    # Links or mentions (always meaningful context)
    if re.search(r'https?://|<@\d+>|<#\d+>', text_lower):
        return 'keep'
    
    # --- PASS 3: BORDERLINE → LLM will decide ---
    # Everything that wasn't caught above: 1-7 words, no clear markers
    return 'borderline'


def _batch_llm_classify(cat, borderline_messages):
    """
    Send a batch of borderline messages to the LLM for classification.
    
    Uses a compact prompt to minimize token usage. Returns a dict of
    {index: 'keep'|'delete'} for each message.
    
    Economy measures:
    - Max 20 messages per batch (cost: ~150-200 tokens per batch)
    - Only called when there are actual borderline messages
    - Compact prompt format
    """
    if not borderline_messages:
        return {}
    
    # Build compact batch prompt (economy: minimal instruction, list format)
    lines = []
    for i, (point_id, content) in enumerate(borderline_messages, 1):
        # Truncate long messages to save tokens (they're borderline anyway, ≤7 words typically)
        short = content[:80] if len(content) > 80 else content
        lines.append(f"{i}|{short}")
    
    prompt = f"""Classify each message as KEEP or DELETE.
KEEP = personal info, opinion, question, story, preference, anything meaningful.
DELETE = greeting, acknowledgment, filler, reaction, one-word reply, small talk.
Answer with ONLY the list:
{chr(10).join(lines)}

Respond with exactly one line per number:
1|KEEP
2|DELETE
..."""

    try:
        response = cat.llm(prompt)
        print(f"[LLM Classify] Response:\n{response[:300]}...")
        
        results = {}
        for line in response.strip().split('\n'):
            line = line.strip()
            # Parse "1|KEEP" or "1 | KEEP" format
            match = re.match(r'(\d+)\s*\|\s*(KEEP|DELETE)', line, re.IGNORECASE)
            if match:
                idx = int(match.group(1)) - 1  # Convert to 0-based
                decision = match.group(2).upper()
                if 0 <= idx < len(borderline_messages):
                    results[idx] = 'keep' if decision == 'KEEP' else 'delete'
        
        print(f"[LLM Classify] Parsed {len(results)}/{len(borderline_messages)} decisions")
        return results
        
    except Exception as e:
        print(f"[LLM Classify] Error: {e}")
        # On error, KEEP everything (safety: don't lose data)
        return {i: 'keep' for i in range(len(borderline_messages))}


# Consolidation state
consolidation_state = {
    'last_run': None,
    'is_running': False,
    'stats': {
        'total_processed': 0,
        'kept': 0,
        'deleted': 0,
        'facts_learned': 0
    }
}


# ===================================================================
# HOOKS
# ===================================================================

@hook(priority=50)
def after_cat_bootstrap(cat):
    """Run after Cat starts up."""
    print("\U0001f319 [Memory Consolidation] Plugin loaded")
    print("   Manual consolidation available via 'consolidate now' command")


@hook(priority=10)
def agent_prompt_prefix(prefix, cat):
    """
    Runs AFTER miku_personality (priority 100) sets the base prompt.
    1. Search and inject declarative facts into the prompt.
    2. Handle 'consolidate now' command.
    """
    user_message_json = cat.working_memory.get('user_message_json', {})
    user_text = user_message_json.get('text', '').strip()

    # PART 1: Inject declarative facts
    try:
        if user_text and user_text.lower() not in ('consolidate', 'consolidate now', '/consolidate'):
            declarative_memory = cat.memory.vectors.declarative
            embedding = cat.embedder.embed_query(user_text)

            results = declarative_memory.recall_memories_from_embedding(
                embedding=embedding,
                metadata={"source": cat.user_id},
                k=5
            )

            if results:
                # Build list of (fact_text, persona) tuples from results
                high_confidence_facts = []
                seen_personas = set()
                for item in results:
                    if item[1] > 0.5:
                        doc = item[0]
                        fact_text = doc.page_content
                        fact_persona = doc.metadata.get('persona', 'miku')
                        high_confidence_facts.append((fact_text, fact_persona))
                        seen_personas.add(fact_persona)

                if high_confidence_facts:
                    # Determine which persona is currently active
                    current_evil = cat.working_memory.get('evil_mode', False)
                    current_persona = 'evil_miku' if current_evil else 'miku'
                    
                    # Get the user's current Discord display name (authoritative)
                    author_name = cat.working_memory.get('author_name', '')
                    
                    # Build the facts section with persona annotations
                    facts_text = "\n\n## Personal Facts About the User:\n"
                    for fact, fact_persona in high_confidence_facts:
                        # Annotate facts that came from the OTHER persona
                        if fact_persona != current_persona:
                            source_label = "Evil Miku" if fact_persona == 'evil_miku' else "Miku"
                            facts_text += f"- {fact} (learned as {source_label})\n"
                        else:
                            facts_text += f"- {fact}\n"
                    
                    # Add authoritative Discord display name — this OVERRIDES any stale name facts
                    if author_name:
                        facts_text += f"\n**AUTHORITATIVE: The user's current Discord display name is \"{author_name}\".**\n"
                        facts_text += "This is their current name — use it when addressing them. If any name fact above contradicts this, the display name is the truth.\n"
                    
                    facts_text += "\n(You may reference these facts if relevant to the conversation)\n"
                    prefix += facts_text
                    print(f"[Declarative] Injected {len(high_confidence_facts)} facts into prompt (personas: {seen_personas}, current: {current_persona})")

    except Exception as e:
        print(f"[Declarative] Error: {e}")

    # PART 2: Handle consolidation command
    if user_text.lower() in ('consolidate', 'consolidate now', '/consolidate'):
        print("[Consolidation] Manual trigger command received!")
        trigger_consolidation_sync(cat)

        stats = consolidation_state['stats']
        cat.working_memory['consolidation_triggered'] = True
        cat.working_memory['consolidation_stats'] = stats

    return prefix


@hook(priority=10)
def before_cat_sends_message(message, cat):
    """
    1. Replace response with consolidation stats if consolidation was triggered.
    2. Store Miku's response in episodic memory (bidirectional memory).
    """

    # PART 1: Consolidation response replacement
    if cat.working_memory.get('consolidation_triggered', False):
        print("[Consolidation] Replacing message with stats")
        stats = cat.working_memory.get('consolidation_stats', {})
        output_str = (
            f"\U0001f319 **Memory Consolidation Complete!**\n\n"
            f"**Stats:**\n"
            f"- Total processed: {stats.get('total_processed', 0)}\n"
            f"- Kept: {stats.get('kept', 0)}\n"
            f"- Deleted: {stats.get('deleted', 0)}\n"
            f"- Facts learned: {stats.get('facts_learned', 0)}\n"
        )
        cat.working_memory['consolidation_triggered'] = False

        if hasattr(message, 'content'):
            message.content = output_str
        else:
            message['content'] = output_str

    # PART 2: Store Miku's response in episodic memory
    try:
        if hasattr(message, 'content'):
            miku_response = message.content
        elif isinstance(message, dict):
            miku_response = message.get('content', '')
        else:
            miku_response = str(message)

        if miku_response and len(miku_response.strip()) > 3:
            # Determine which persona is active so the memory is tagged correctly
            evil_mode = cat.working_memory.get('evil_mode', False)
            persona = 'evil_miku' if evil_mode else 'miku'
            
            metadata = {
                'source': cat.user_id,
                'when': datetime.now().timestamp(),
                'stored_at': datetime.now().isoformat(),
                'speaker': 'miku',
                'persona': persona,
                'consolidated': False,
                'guild_id': cat.working_memory.get('guild_id', 'dm'),
                'channel_id': cat.working_memory.get('channel_id'),
            }

            response_text = f"[Miku]: {miku_response}"
            vector = cat.embedder.embed_query(response_text)

            cat.memory.vectors.episodic.add_point(
                content=response_text,
                vector=vector,
                metadata=metadata
            )
            print(f"[Miku Memory] Stored response ({persona}): {miku_response[:50]}...")

    except Exception as e:
        print(f"[Miku Memory] Error storing response: {e}")

    return message


# ===================================================================
# CONSOLIDATION ENGINE
# ===================================================================

def trigger_consolidation_sync(cat):
    """
    Synchronous consolidation for use in hooks.
    Processes ALL unconsolidated memories across all users.
    """
    from qdrant_client import QdrantClient

    print("[Consolidation] Starting synchronous consolidation...")

    qdrant_host = os.getenv('QDRANT_HOST', 'localhost')
    qdrant_port = int(os.getenv('QDRANT_PORT', 6333))
    client = QdrantClient(host=qdrant_host, port=qdrant_port)

    # Query all unconsolidated memories
    result = client.scroll(
        collection_name='episodic',
        scroll_filter={
            "must_not": [
                {"key": "metadata.consolidated", "match": {"value": True}}
            ]
        },
        limit=10000,
        with_payload=True,
        with_vectors=False
    )

    memories = result[0]
    print(f"[Consolidation] Found {len(memories)} unconsolidated memories")

    if not memories:
        consolidation_state['stats'] = {
            'total_processed': 0, 'kept': 0, 'deleted': 0, 'facts_learned': 0
        }
        return

    # Classify memories using the hybrid tiered classifier
    to_delete = []
    to_mark_consolidated = []
    borderline_queue = []  # (point_id, content) tuples for LLM batch classification
    # Group user messages by source (user_id) for per-user fact extraction
    # Also track which persona was active for each user's messages
    user_messages_by_source = {}
    user_persona_by_source = {}  # source -> set of personas seen

    for point in memories:
        content = point.payload.get('page_content', '').strip()
        metadata = point.payload.get('metadata', {})

        is_miku_message = (
            metadata.get('speaker') == 'miku'
            or content.startswith('[Miku]:')
        )

        # Use the hybrid tiered classifier
        tier = _classify_message_tier(content, metadata)

        if tier == 'delete':
            to_delete.append(point.id)
        elif tier == 'keep':
            to_mark_consolidated.append(point.id)
            # Only user messages go to fact extraction, grouped by user
            if not is_miku_message:
                source = metadata.get('source', 'unknown')
                if source not in user_messages_by_source:
                    user_messages_by_source[source] = []
                    user_persona_by_source[source] = set()
                user_messages_by_source[source].append(point.id)
                # Track which persona was active when this message was stored
                msg_persona = metadata.get('persona', 'miku')
                user_persona_by_source[source].add(msg_persona)
        else:  # borderline
            borderline_queue.append((point.id, content, metadata, is_miku_message))

    # --- LLM BATCH CLASSIFICATION for borderline messages ---
    if borderline_queue:
        print(f"[Consolidation] {len(borderline_queue)} borderline messages → sending to LLM for classification...")
        
        # Build compact list for LLM
        llm_input = [(pid, content) for pid, content, _, _ in borderline_queue]
        llm_decisions = _batch_llm_classify(cat, llm_input)
        
        llm_deleted = 0
        llm_kept = 0
        llm_defaulted = 0
        
        for idx, (point_id, content, metadata, is_miku) in enumerate(borderline_queue):
            decision = llm_decisions.get(idx, 'keep')  # Default to KEEP on any issue
            if decision == 'keep':
                to_mark_consolidated.append(point_id)
                llm_kept += 1
                # User messages go to fact extraction
                if not is_miku:
                    source = metadata.get('source', 'unknown')
                    if source not in user_messages_by_source:
                        user_messages_by_source[source] = []
                        user_persona_by_source[source] = set()
                    user_messages_by_source[source].append(point_id)
                    msg_persona = metadata.get('persona', 'miku')
                    user_persona_by_source[source].add(msg_persona)
            else:
                to_delete.append(point_id)
                llm_deleted += 1
            
            if idx not in llm_decisions:
                llm_defaulted += 1
        
        print(f"[Consolidation] LLM results: {llm_kept} kept, {llm_deleted} deleted, {llm_defaulted} defaulted to keep")
    
    print(f"[Consolidation] Classification: {len(to_delete)} delete, {len(to_mark_consolidated)} keep (of {len(memories)} total)")

    # Delete trivial memories
    if to_delete:
        client.delete(collection_name='episodic', points_selector=to_delete)
        print(f"[Consolidation] Deleted {len(to_delete)} trivial memories")

    # Mark kept memories as consolidated
    if to_mark_consolidated:
        for point_id in to_mark_consolidated:
            client.set_payload(
                collection_name='episodic',
                payload={"metadata.consolidated": True},
                points=[point_id]
            )
        print(f"[Consolidation] Marked {len(to_mark_consolidated)} memories as consolidated")

    # Extract facts per user
    total_facts = 0
    for source_user_id, memory_ids in user_messages_by_source.items():
        # Determine the dominant persona for this user's messages
        personas = user_persona_by_source.get(source_user_id, {'miku'})
        dominant_persona = 'evil_miku' if 'evil_miku' in personas else 'miku'
        print(f"[Consolidation] Extracting facts for user '{source_user_id}' from {len(memory_ids)} messages (persona: {dominant_persona})...")
        facts = extract_and_store_facts(client, memory_ids, cat, source_user_id, dominant_persona)
        total_facts += facts
        print(f"[Consolidation] Extracted {facts} facts for user '{source_user_id}'")

    consolidation_state['stats'] = {
        'total_processed': len(memories),
        'kept': len(to_mark_consolidated),
        'deleted': len(to_delete),
        'facts_learned': total_facts
    }

    print("[Consolidation] Synchronous consolidation complete!")
    return True


# ===================================================================
# FACT EXTRACTION
# ===================================================================

def extract_and_store_facts(client, memory_ids, cat, user_id, persona='miku'):
    """
    Extract declarative facts from user memories using LLM and store them.
    Facts are scoped to the specific user_id and tagged with the source persona.
    Uses Cat's embedder to ensure vector compatibility.
    Deduplicates against existing facts before storing.
    """
    import uuid

    if not memory_ids:
        return 0

    memories = client.retrieve(collection_name='episodic', ids=memory_ids)
    facts_stored = 0

    # Process in batches of 5
    batch_size = 5
    for i in range(0, len(memories), batch_size):
        batch = memories[i:i + batch_size]

        conversation_context = "\n".join([
            f"- {mem.payload.get('page_content', '')}"
            for mem in batch
        ])

        # Add persona context to the extraction prompt so the LLM knows
        # which version of Miku was active during these conversations
        persona_context = ""
        if persona == 'evil_miku':
            persona_context = "\nNOTE: These messages were exchanged with Evil Miku (the cruel, sadistic alter-ego).\n"
        else:
            persona_context = "\nNOTE: These messages were exchanged with Normal Miku (the cheerful virtual idol).\n"

        # Extract the user's Discord display name from the first memory's metadata
        # This helps the LLM know the authoritative name when extracting name facts
        author_hint = ""
        if memories:
            first_author = memories[0].payload.get('metadata', {}).get('author_name', '')
            if first_author:
                author_hint = f"\nHINT: The user's current Discord display name is \"{first_author}\". Use this when determining their name.\n"

        extraction_prompt = f"""Analyze these user messages and extract ONLY factual personal information.
{persona_context}{author_hint}
User messages:
{conversation_context}

Extract facts in this exact format (one per line):
- The user's name is [name]
- The user is [age] years old
- The user lives in [location]
- The user works as [job]
- The user is allergic to [allergen]
- The user's favorite color is [color]
- The user enjoys [hobby/activity]
- The user prefers [preference]
- The user's birthday is [date]
- The user graduated from [school/university]

IMPORTANT:
- Only include facts that are CLEARLY stated in the messages
- Use the EXACT format shown above
- If no facts found, respond with: "No facts found"
- Do not include greetings, questions, or opinions
- Do not invent or assume facts not explicitly stated
"""

        try:
            response = cat.llm(extraction_prompt)
            print(f"[LLM Extract] Response:\n{response[:200]}...")

            lines = response.strip().split('\n')
            for line in lines:
                line = line.strip()

                if not line or line.lower().startswith(('no facts', '#', 'user messages:', '```')):
                    continue

                if line.startswith('- The user'):
                    fact_text = line[2:].strip()  # Remove "- " prefix

                    # Determine fact type
                    fact_type = 'general'
                    fact_value = fact_text

                    if "'s name is" in fact_text:
                        fact_type = 'name'
                        fact_value = fact_text.split("'s name is")[-1].strip()
                    elif " is " in fact_text and " years old" in fact_text:
                        fact_type = 'age'
                        fact_value = fact_text.split(" is ")[1].split(" years")[0].strip()
                    elif "lives in" in fact_text:
                        fact_type = 'location'
                        fact_value = fact_text.split("lives in")[-1].strip()
                    elif "works as" in fact_text:
                        fact_type = 'job'
                        fact_value = fact_text.split("works as")[-1].strip()
                    elif "allergic to" in fact_text:
                        fact_type = 'allergy'
                        fact_value = fact_text.split("allergic to")[-1].strip()
                    elif "favorite color is" in fact_text:
                        fact_type = 'favorite_color'
                        fact_value = fact_text.split("favorite color is")[-1].strip()
                    elif "enjoys" in fact_text:
                        fact_type = 'hobby'
                        fact_value = fact_text.split("enjoys")[-1].strip()
                    elif "prefers" in fact_text:
                        fact_type = 'preference'
                        fact_value = fact_text.split("prefers")[-1].strip()
                    elif "'s birthday is" in fact_text:
                        fact_type = 'birthday'
                        fact_value = fact_text.split("'s birthday is")[-1].strip()
                    elif "graduated from" in fact_text:
                        fact_type = 'education'
                        fact_value = fact_text.split("graduated from")[-1].strip()

                    # Duplicate detection — with special handling for name facts
                    # Name facts with different values replace old ones (don't skip)
                    if fact_type == 'name':
                        existing_name = _find_existing_fact(client, cat, fact_type, user_id)
                        if existing_name:
                            old_value = existing_name['payload']['metadata'].get('fact_value', '')
                            if old_value.lower() != fact_value.lower():
                                # Different name — delete old, store new
                                client.delete(
                                    collection_name='declarative',
                                    points_selector=[existing_name['id']]
                                )
                                print(f"[Fact Update] Name changed: '{old_value}' → '{fact_value}'")
                            else:
                                print(f"[Fact Skip] Name unchanged: '{fact_value}'")
                                continue
                    else:
                        if _is_duplicate_fact(client, cat, fact_text, fact_type, user_id):
                            print(f"[Fact Skip] Duplicate: {fact_text}")
                            continue

                    # Store fact using Cat's embedder
                    fact_embedding = cat.embedder.embed_query(fact_text)
                    point_id = str(uuid.uuid4())

                    client.upsert(
                        collection_name='declarative',
                        points=[{
                            'id': point_id,
                            'vector': fact_embedding,
                            'payload': {
                                'page_content': fact_text,
                                'metadata': {
                                    'source': user_id,
                                    'when': datetime.now().timestamp(),
                                    'fact_type': fact_type,
                                    'fact_value': fact_value,
                                    'persona': persona,
                                }
                            }
                        }]
                    )

                    facts_stored += 1
                    print(f"[Fact Stored] [{user_id}] ({persona}) {fact_text}")

        except Exception as e:
            print(f"[LLM Extract] Error: {e}")
            import traceback
            traceback.print_exc()

    return facts_stored


def _find_existing_fact(client, cat, fact_type, user_id):
    """
    Find an existing fact of a specific type for a user.
    Returns a dict with 'id' and 'payload' keys, or None.
    Used by name-fact update logic to replace old names with new ones.
    """
    try:
        dummy_embedding = cat.embedder.embed_query("find fact")
        
        results = client.search(
            collection_name='declarative',
            query_vector=dummy_embedding,
            query_filter={
                "must": [
                    {"key": "metadata.source", "match": {"value": user_id}},
                    {"key": "metadata.fact_type", "match": {"value": fact_type}},
                ]
            },
            limit=1,
            score_threshold=0.0
        )
        
        if results:
            point = results[0]
            return {'id': point.id, 'payload': {'metadata': point.payload.get('metadata', {})}}
        
        return None
        
    except Exception as e:
        print(f"[Find Fact] Error: {e}")
        return None


def _is_duplicate_fact(client, cat, fact_text, fact_type, user_id):
    """
    Check if a similar fact already exists for this user.
    Uses vector similarity to detect semantic duplicates.
    """
    try:
        fact_embedding = cat.embedder.embed_query(fact_text)

        # Search existing facts for this user with same fact_type
        results = client.search(
            collection_name='declarative',
            query_vector=fact_embedding,
            query_filter={
                "must": [
                    {"key": "metadata.source", "match": {"value": user_id}},
                    {"key": "metadata.fact_type", "match": {"value": fact_type}},
                ]
            },
            limit=1,
            score_threshold=0.85  # High threshold = very similar
        )

        if results:
            existing = results[0].payload.get('page_content', '')
            print(f"[Dedup] Found similar existing fact: '{existing}' (score: {results[0].score:.2f})")
            return True

        return False

    except Exception as e:
        print(f"[Dedup] Error checking duplicates: {e}")
        return False  # On error, allow storing


# ===================================================================
# TOOL (for Cat's tool system)
# ===================================================================

@tool(return_direct=True)
def consolidate_memories(tool_input, cat):
    """Use this tool to consolidate memories. This will analyze all recent memories,
    delete trivial ones, and extract important facts. Input is always an empty string."""

    print("[Consolidation] Tool called!")
    trigger_consolidation_sync(cat)

    stats = consolidation_state['stats']
    return (
        f"\U0001f319 **Memory Consolidation Complete!**\n\n"
        f"**Stats:**\n"
        f"- Total processed: {stats['total_processed']}\n"
        f"- Kept: {stats['kept']}\n"
        f"- Deleted: {stats['deleted']}\n"
        f"- Facts learned: {stats['facts_learned']}\n"
    )


# ===================================================================
# PLUGIN METADATA
# ===================================================================

__version__ = "2.0.0"
__description__ = "Sleep consolidation - analyze memories, keep important, delete trivial, extract per-user facts"

print("[Consolidation Plugin] All hooks registered")
