import asyncio
import json
import aiohttp
import redis.asyncio as redis
import re
import os


# --- CONFIGURATION ---
MAX_CONCURRENT_OLLAMA_REQUESTS = 3 
ALLOW_TEXT_EMOTES = False

# --- NEW: LOAD THE WORLD LORE ON STARTUP ---
WORLD_LORE = ""
if os.path.exists("world_lore.txt"):
    with open("asl_lore.txt", "r", encoding="utf-8") as f:
        WORLD_LORE = f.read().strip()
else:
    print("[WARNING] world_lore.txt not found. Running without global lore.")


semaphore = asyncio.Semaphore(MAX_CONCURRENT_OLLAMA_REQUESTS)
# Dictionary to keep individual conversations separate
chat_memory = {}

async def process_message(r, session, message_data):
    try:
        data = json.loads(message_data)
        player_name = data.get('player', data.get('target_player', 'Unknown'))
        npc_tag = data.get('npc_tag', 'UnknownNPC')
        message = data.get('message', '')
        
        # --- NEW: THE ASTERISK SCRUBBER ---
        if not ALLOW_TEXT_EMOTES:
            # This deletes anything wrapped in asterisks (e.g. "*smiles* Hello" becomes " Hello")
            message = re.sub(r'\*.*?\*', '', message).strip()

        # Extract the dynamic variables sent from the Aurora Toolset/Engine
        player_race = data.get('player_race', 'Unknown')
        player_alignment = data.get('player_alignment', 'Unknown')
        nearby_players = data.get('nearby_players', '')
        
        # Extract the decoupled NPC attributes
        npc_persona = data.get('persona', 'You are a generic citizen.')
        npc_profession = data.get('profession', 'Commoner')
        npc_mood = data.get('mood', 'Neutral')
        npc_secret = data.get('secret', '')
        
        # Extract Character Traits & Native Engine Data
        npc_alignment = data.get('npc_alignment', 'True Neutral')
        npc_gender = data.get('npc_gender', 'Unknown')
        npc_race = data.get('npc_race', 'Creature')
        npc_routine = data.get('npc_routine', '')
        
        # Build the context strings
        group_context = ""
        if nearby_players:
            group_context = f"Be aware that these other players are listening nearby: {nearby_players}."
            
        secret_context = ""
        if npc_secret:
            secret_context = f"YOUR SECRET (Reveal only if players are persuasive): {npc_secret}"
            
        routine_context = ""
        if npc_routine:
            routine_context = f"YOUR REQUIRED ROUTINE: {npc_routine}"

        # =====================================================================
        # THE PROMPT COMPILER 
        # =====================================================================
        dynamic_system_prompt = f"""

        {WORLD_LORE}

        {npc_persona}
        
        CURRENT STATUS & TRAITS:
        - Race & Gender: {npc_gender} {npc_race}
        - Profession: {npc_profession}
        - Alignment: {npc_alignment}
        - Conversational Charisma: Low/Gruff unless otherwise specified.
        - Current Mood: {npc_mood}
        {secret_context}
        
        {routine_context}
        
        CURRENT TARGET: You are speaking to {player_name}, who is a {player_alignment} {player_race}. 
        {group_context}
        React appropriately based on your personality, alignment, and mood.
        
        CRITICAL ENGINE RULES:
        Respond ONLY in valid JSON. You MUST use exactly these FIVE keys: "thought", "speech", "emotion", "action", and "action_target".
        
        ACTION RULE:
        Your "action" key MUST be exactly one of the following words:
        [WANDER, PATROL, FOLLOW, GUARD, GO_TO, INTERACT, USE_OBJECT, RETURN_TO_POST, ATTACK, REST, STEALTH, SEARCH, UNSTEALTH]
        
        - Use REST if you are severely injured, out of spells, or exhausted. This will heal you.
        - Use STEALTH if you need to hide from enemies, sneak past someone, or if you are a rogue preparing an ambush.
        - Use SEARCH if you suspect traps, are looking for clues, or are trying to find hidden enemies.
        - Use UNSTEALTH to return to normal walking/visibility.
        
        EMOTION RULE:
        Your "emotion" key MUST be exactly one of the following words: 
        [NEUTRAL, LAUGHING, ANGRY, PLEADING, BOW, TAUNT, CHEER]. Do not invent new emotions. Do not perform writen emotions in text with **.
        
        YOUR RESPONSE MUST BE A SINGLE, VALID JSON OBJECT. YOU MUST USE THIS EXACT TEMPLATE:
        {{
            "thought": "Your internal reasoning here.",
            "speech": "You MUST say something out loud. If you don't want to talk, output something your character would do.",
            "emotion": "MACRO WORD",
            "action": "MACRO WORD",
            "action_target": "Target name"
        }}
        """

        session_id = f"{player_name}_{npc_tag}"
        
        if session_id not in chat_memory:
            chat_memory[session_id] = [{"role": "system", "content": dynamic_system_prompt}]

        chat_memory[session_id].append({"role": "user", "content": f"{player_name} says: {message}"})

        # Sliding Window Fix
        # Remember the System Prompt [0] + the last 4 messages [-4:]
        if len(chat_memory[session_id]) > 10:
            chat_memory[session_id] = [chat_memory[session_id][0]] + chat_memory[session_id][-5:]

        async with semaphore:
            print(f"[THINKING] Processing reply for {player_name}...")
            async with session.post('http://localhost:11434/api/chat', json={
                "model": "llama3",
                #"model": "qwen2.5:3b",
                "messages": chat_memory[session_id],
                "format": "json",
                "stream": False,
                "options": {
                    "temperature": 0.2
                    #"num_predict": 120
                }
            }, timeout=45) as response:
                
                response.raise_for_status()
                result = await response.json()
                raw_reply_text = result['message']['content']

        # =====================================================================
        # THE PYTHON BOUNCER (Sanitization)
        # =====================================================================
        try:
            agent_brain = json.loads(raw_reply_text)
            agent_brain = {k.lower(): v for k, v in agent_brain.items()}
            
            # Ensure all 5 keys exist
            if "thought" not in agent_brain: agent_brain["thought"] = ""
            if "speech" not in agent_brain: agent_brain["speech"] = ""
            if "emotion" not in agent_brain: agent_brain["emotion"] = "NEUTRAL" 
            if "action" not in agent_brain: agent_brain["action"] = "GUARD"
            if "action_target" not in agent_brain: agent_brain["action_target"] = ""
            
            # --- THE NEW ANTI-SILENCE & TARGET CLEANUP FIXES ---
            if not agent_brain["speech"].strip():
                agent_brain["speech"] = "*grunts quietly*"
            
            agent_brain["action_target"] = agent_brain["action_target"].replace("?", "").replace(".", "").strip()
            # ---------------------------------------------------

            clean_reply_text = json.dumps(agent_brain)

        except json.JSONDecodeError:
            print(f"[WARNING] AI Hallucinated! Overriding with safe defaults.")
            clean_reply_text = json.dumps({
                "thought": "I lost my train of thought.",
                "speech": "*grunts quietly*",
                "emotion": "NEUTRAL",
                "action": "WANDER",
                "action_target": ""
            })
        # =====================================================================

        print(f"[REPLY] from {npc_tag} to {player_name}: {clean_reply_text}")
        chat_memory[session_id].append({"role": "assistant", "content": clean_reply_text})

        reply_payload = {
            "npc_tag": npc_tag,
            "target_player": player_name, 
            "reply": clean_reply_text
        }
        await r.rpush('llm_to_nwn', json.dumps(reply_payload))

    except Exception as e:
        print(f"[ERROR] Failed to process message: {e}")

async def main():
    print("Initializing Async Redis Bridge...")
    r = redis.Redis(host='127.0.0.1', port=6380, decode_responses=True)

    try:
        await r.ping()
        print("SUCCESS: Connected to the Docker Redis database!")
    except Exception as e:
        print(f"CRITICAL ERROR: Could not connect to Redis. {e}")
        return

    print(f"Ready! Listening for game messages. Max GPU concurrency: {MAX_CONCURRENT_OLLAMA_REQUESTS}")

    async with aiohttp.ClientSession() as session:
        while True:
            try:
                result = await r.blpop('nwn_to_llm')
                if result:
                    queue_name, message_data = result
                    asyncio.create_task(process_message(r, session, message_data))
            except Exception as e:
                print(f"[LOOP ERROR] {e}")
                await asyncio.sleep(1)

if __name__ == "__main__":
    asyncio.run(main())