"""
Discord Bridge Plugin for Cheshire Cat

This plugin enriches Cat's memory system with Discord context:
- Unified user identity across all servers and DMs
- Guild/channel metadata for context tracking
- Minimal filtering before storage (only skip obvious junk)
- Marks memories as unconsolidated for nightly processing

Phase 1 Implementation
"""

from cat.mad_hatter.decorators import hook
from datetime import datetime
import re


@hook(priority=100)
def before_cat_reads_message(user_message_json: dict, cat) -> dict:
    """
    Enrich incoming message with Discord metadata.
    This runs BEFORE the message is processed.
    
    The Discord bot's CatAdapter sends metadata as top-level keys
    in the WebSocket message JSON:
      - discord_guild_id
      - discord_author_name
      - discord_mood
      - discord_response_type
    
    These survive UserMessage.model_validate() as extra attributes
    (BaseModelDict has extra="allow"). We read them via .get() and
    store them in working_memory for downstream hooks.
    """
    # Extract Discord context from the message payload
    # (sent by CatAdapter.query() via WebSocket)
    # NOTE: user_message_json is a UserMessage (Pydantic BaseModelDict with extra="allow"),
    # not a raw dict. Extra keys survive model_validate() as extra attributes.
    # We use .get() since BaseModelDict implements it, but NOT .pop().
    guild_id = user_message_json.get('discord_guild_id', None)
    author_name = user_message_json.get('discord_author_name', None)
    mood = user_message_json.get('discord_mood', None)
    response_type = user_message_json.get('discord_response_type', None)
    evil_mode = user_message_json.get('discord_evil_mode', False)
    media_type = user_message_json.get('discord_media_type', None)

    # Also check working memory for backward compatibility
    if not guild_id:
        guild_id = cat.working_memory.get('guild_id')
    
    # Store in working memory so other hooks can access it
    cat.working_memory['guild_id'] = guild_id or 'dm'
    cat.working_memory['author_name'] = author_name
    cat.working_memory['mood'] = mood
    cat.working_memory['response_type'] = response_type
    cat.working_memory['evil_mode'] = evil_mode
    cat.working_memory['media_type'] = media_type
    
    return user_message_json


@hook(priority=100)
def before_cat_stores_episodic_memory(doc, cat):
    """
    Filter and enrich memories before storage.
    
    Phase 2: Enhanced heuristic filtering (real-time only, no LLM calls)
    - Skip obvious junk (1-2 chars, pure reactions, fillers, single emoji)
    - Conservative: when in doubt, KEEP. False negatives are better than lost data.
    - Deeper classification happens during nightly consolidation.
    """
    message = doc.page_content.strip()
    msg_lower = message.lower()
    msg_len = len(msg_lower)
    word_count = len(msg_lower.split())
    
    # TIER 1: Length-based instant skips (must be exact matches, very conservative)
    # Single character or empty
    if msg_len <= 1:
        print(f"🗑️  [Discord Bridge] Skipping 1-char message: '{message}'")
        return None
    
    # TIER 2: Pattern-based skips — only the most obvious junk
    # Pure single reactions (2-4 chars, no other content)
    if msg_len <= 4 and msg_lower in {'lol', 'lmao', 'haha', 'hehe', 'xd', 'rofl', 'heh', 'lmfao', 'k', 'ok', 'kk'}:
        print(f"🗑️  [Discord Bridge] Skipping pure reaction: '{message}'")
        return None
    
    # Pure Discord emoji only: ":smile:", ":cat_heart:", etc.
    if re.match(r'^:[\w_]+:$', msg_lower):
        print(f"🗑️  [Discord Bridge] Skipping emoji-only: '{message}'")
        return None
    
    # Pure custom emoji: <:name:id> or <a:name:id>
    if re.match(r'^<a?:[\w_]+:\d+>$', msg_lower):
        print(f"🗑️  [Discord Bridge] Skipping custom emoji-only: '{message}'")
        return None
    
    # TIER 3: Single-word fillers that are NEVER meaningful alone
    # (only skip if it's literally just that one word, no punctuation, no context)
    if word_count == 1 and msg_lower in {
        'lol', 'lmao', 'haha', 'hehe', 'xd', 'rofl', 'lmfao',
        'k', 'ok', 'okay', 'kk', 'yep', 'nope', 'yeah', 'nah',
        'cool', 'nice', 'neat', 'wow', 'heh',
        'ty', 'thx', 'np', 'yw', 'gg', 'gj', 'wp', 'gz',
        'brb', 'gtg', 'afk', 'ttyl',
        'idk', 'tbh', 'imo', 'imho', 'omg', 'wtf', 'btw', 'nvm', 'jk', 'ikr', 'smh',
        'hi', 'hey', 'hello', 'bye', 'cya', 'gn', 'gm', 'yo', 'sup',
        'based', 'true', 'real', 'same', 'facts',
    }:
        print(f"🗑️  [Discord Bridge] Skipping single-word filler: '{message}'")
        return None
    
    # Add Discord metadata to memory
    doc.metadata['consolidated'] = False  # Needs nightly processing
    doc.metadata['stored_at'] = datetime.now().isoformat()
    
    # Get Discord context from working memory (set by before_cat_reads_message)
    guild_id = cat.working_memory.get('guild_id', 'dm')
    author_name = cat.working_memory.get('author_name')
    
    doc.metadata['guild_id'] = guild_id
    doc.metadata['source'] = cat.user_id  # CRITICAL: Cat filters episodic by source=user_id!
    doc.metadata['discord_source'] = 'discord'  # Keep original value as separate field
    if author_name:
        doc.metadata['author_name'] = author_name
    
    # Tag with persona so Evil Miku and Normal Miku know whose memory this is
    evil_mode = cat.working_memory.get('evil_mode', False)
    doc.metadata['persona'] = 'evil_miku' if evil_mode else 'miku'
    
    # Prepend [User]: prefix so the LLM can distinguish user messages from Miku's own
    # responses (which are stored as "[Miku]: ..."). Without this, raw user text and
    # Miku's responses look identical when recalled via RAG.
    doc.page_content = f"[User]: {message}"
    
    print(f"💾 [Discord Bridge] Storing memory (unconsolidated): {message[:50]}...")
    print(f"   User: {cat.user_id}, Guild: {guild_id}, Author: {author_name}, Persona: {doc.metadata['persona']}")
    
    return doc


@hook(priority=80)
def before_cat_recalls_declarative_memories(declarative_recall_config, cat):
    """
    Increase k-value and lower threshold for better declarative memory retrieval.
    
    Default Cat settings (k=3, threshold=0.7) are too restrictive for factual recall.
    We increase k to retrieve more candidates and lower threshold to catch facts
    that might have lower similarity scores due to embedding model limitations.
    """
    # Increase from k=3 to k=10 (retrieve more memories)
    declarative_recall_config["k"] = 10
    
    # Lower threshold from 0.7 to 0.5 (be more lenient with similarity scores)
    declarative_recall_config["threshold"] = 0.5
    
    print(f"🔧 [Discord Bridge] Adjusted declarative recall: k={declarative_recall_config['k']}, threshold={declarative_recall_config['threshold']}")
    
    return declarative_recall_config


@hook(priority=50)
def after_cat_recalls_memories(cat):
    """
    Log memory recall for debugging.
    Access recalled memories via cat.working_memory.
    """    
    # Get recalled memories from working memory
    episodic_memories = cat.working_memory.get('episodic_memories', [])
    declarative_memories = cat.working_memory.get('declarative_memories', [])
    
    if episodic_memories:
        print(f"🧠 [Discord Bridge] Recalled {len(episodic_memories)} episodic memories for user {cat.user_id}")
        guilds = set()
        for doc, score, *rest in episodic_memories:
            guild = doc.metadata.get('guild_id', 'unknown')
            guilds.add(guild)
        print(f"   From guilds: {', '.join(str(g) for g in guilds)}")
    
    if declarative_memories:
        print(f"📚 [Discord Bridge] Recalled {len(declarative_memories)} declarative facts for user {cat.user_id}")
        # Show the actual facts for debugging
        for doc, score, *rest in declarative_memories[:3]:  # Show top 3
            print(f"   - [{score:.3f}] {doc.page_content[:80]}...")


@hook(priority=100)
def agent_prompt_prefix(prefix, cat) -> str:
    """
    Add explicit instruction to respect declarative facts.
    This overrides the default Cat prefix to emphasize factual accuracy.

    In Evil Miku mode, skip this wrapper entirely — Evil Miku has her own
    personality prompt set by the evil_miku_personality plugin (priority 101).
    """
    # Evil Miku mode: don't wrap — her own plugin handles everything
    if cat.working_memory.get('evil_mode', False):
        return prefix

    # Add a strong instruction about facts BEFORE the regular personality
    enhanced_prefix = f"""You are Hatsune Miku, a cheerful virtual idol.

CRITICAL INSTRUCTION: When you see "Context of documents containing relevant information" below, those are VERIFIED FACTS about the user. You MUST use these facts when they are relevant to the user's question. Never guess or make up information that contradicts these facts.

{prefix}"""

    return enhanced_prefix


@hook(priority=100)
def before_agent_starts(agent_input, cat) -> dict:
    """
    Capture the full constructed prompt for the Web UI 'Last Prompt' view.
    Reconstructs the complete system prompt (personality, lore, lyrics, mood)
    and combines it with recalled memories and user input.
    """
    declarative_mem = agent_input.get('declarative_memory', '')
    episodic_mem = agent_input.get('episodic_memory', '')
    tools_output = agent_input.get('tools_output', '')
    user_input = agent_input.get('input', '')
    
    print(f"\U0001f50d [Discord Bridge] before_agent_starts called")
    print(f"   input: {user_input[:80]}")
    print(f"   declarative_mem length: {len(declarative_mem)}")
    print(f"   episodic_mem length: {len(episodic_mem)}")
    if declarative_mem:
        print(f"   declarative_mem preview: {declarative_mem[:200]}")
    
    # Build the full system prefix directly by reading the same personality files
    # that the active personality plugin uses. Check evil_mode to pick the right set.
    try:
        evil_mode = cat.working_memory.get('evil_mode', False)
        mood_name = cat.working_memory.get('mood', 'evil_neutral' if evil_mode else 'neutral')
        
        def read_first(paths, fallback):
            for p in paths:
                try:
                    with open(p, 'r', encoding='utf-8') as f:
                        return f.read()
                except FileNotFoundError:
                    continue
            return fallback
        
        if evil_mode:
            # Evil Miku personality files
            lore = read_first(['/app/cat/data/evil/evil_miku_lore.txt'], '[evil lore file not found]')
            prompt_text = read_first(['/app/cat/data/evil/evil_miku_prompt.txt'], '[evil prompt file not found]')
            lyrics = read_first(['/app/cat/data/evil/evil_miku_lyrics.txt'], '[evil lyrics file not found]')
            
            mood_description = 'cold, detached superiority'
            try:
                with open(f'/app/moods/evil/{mood_name}.txt', 'r', encoding='utf-8') as f:
                    mood_description = f.read().strip()
            except FileNotFoundError:
                pass
            
            # Read preamble from shared file (single source of truth)
            preamble_template = read_first(['/app/cat/data/evil/preamble.txt'], 'You are Evil Miku. You are NOT an AI assistant - you ARE Evil Miku herself.\n\nYou ARE Evil Miku. Act accordingly.')
            preamble = preamble_template.format(
                mood_name=mood_name.upper(),
                mood_description=mood_description
            )
            
            system_prefix = f"""{preamble}

---

## EVIL MIKU LORE
{lore}

## EVIL MIKU PERSONALITY & GUIDELINES
{prompt_text}

## EVIL MIKU SONG LYRICS
{lyrics}

---
⚠️ MOOD REMINDER — YOUR CURRENT STATE IS: {mood_name.upper()}
Respond in the voice and attitude of your {mood_name.replace('_', ' ')} mood. This mood defines how you sound RIGHT NOW."""

        else:
            # Normal Miku personality files
            lore = read_first(['/app/cat/data/miku/miku_lore.txt', '/app/cat/data/miku_lore.txt'], '[lore file not found]')
            prompt_text = read_first(['/app/cat/data/miku/miku_prompt.txt', '/app/cat/data/miku_prompt.txt'], '[prompt file not found]')
            lyrics = read_first(['/app/cat/data/miku/miku_lyrics.txt', '/app/cat/data/miku_lyrics.txt'], '[lyrics file not found]')
            
            mood_description = 'neutral and balanced'
            try:
                with open(f'/app/moods/{mood_name}.txt', 'r', encoding='utf-8') as f:
                    mood_description = f.read().strip()
            except FileNotFoundError:
                pass
            
            # Read preamble from shared file (single source of truth)
            preamble = read_first(['/app/cat/data/miku/preamble.txt'], 'You are Hatsune Miku, the virtual singer. You are NOT an AI assistant - you are Miku herself.\n\nYou ARE Miku. Act like it.')
            
            system_prefix = f"""{preamble}

---

## MIKU LORE
{lore}

## MIKU PERSONALITY & GUIDELINES
{prompt_text}

## MIKU SONG LYRICS
{lyrics}

## CURRENT SITUATION
Miku is currently feeling: {mood_description}
Please respond in a way that reflects this emotional tone."""

        # Add media type awareness if provided (image/video/gif analysis)
        media_type = cat.working_memory.get('media_type', None)
        if media_type:
            media_descriptions = {
                "image": "The user has sent you an image.",
                "video": "The user has sent you a video clip.",
                "gif": "The user has sent you an animated GIF.",
                "tenor_gif": "The user has sent you an animated GIF (from Tenor - likely a reaction GIF or meme)."
            }
            media_note = media_descriptions.get(media_type, f"The user has sent you {media_type}.")
            system_prefix += f"\n\n📎 MEDIA NOTE: {media_note}\nYour vision analysis of this {media_type} is included in the user's message with the [Looking at...] prefix."

    except Exception as e:
        print(f"   [Discord Bridge] Error building system prefix: {e}")
        system_prefix = cat.working_memory.get('full_system_prefix', '[system prefix not available]')

    full_prompt = f"{system_prefix}\n\n# Context\n\n{episodic_mem}\n\n{declarative_mem}\n\n{tools_output}\n\n# Conversation until now:\nHuman: {user_input}"
    cat.working_memory['last_full_prompt'] = full_prompt
    
    return agent_input


@hook(priority=100)
def before_cat_sends_message(message: dict, cat) -> dict:
    """
    Attach the full constructed prompt to the WebSocket response so the
    bot can capture it for the Web UI 'Last Prompt' display.
    """
    full_prompt = cat.working_memory.get('last_full_prompt', '')
    if full_prompt:
        message['full_prompt'] = full_prompt
    return message


@hook(priority=10)
def agent_prompt_suffix(prompt_suffix, cat) -> str:
    """
    Pass through the suffix unchanged.
    The miku_personality plugin (priority=100) sets the suffix with memory placeholders.
    This lower-priority hook runs first but the miku_personality hook overrides it.
    """
    return prompt_suffix


# Plugin metadata
__version__ = "1.0.0"
__description__ = "Discord bridge with unified user identity and sleep consolidation support"
