#!/usr/bin/env python3
"""
Morrowind MCP Server — Timmy's game interface.

Exposes Morrowind as tools via MCP (stdio transport).
Perception comes from the OpenMW log file.
Actions go via CGEvent keypresses to the game window.

Register in ~/.hermes/config.yaml:
  mcp_servers:
    morrowind:
      command: "python3"
      args: ["/Users/apayne/.timmy/morrowind/mcp_server.py"]
"""

import json
import os
import re
import sys
import time
import subprocess

# MCP SDK
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent

# ═══════════════════════════════════════
# CONFIG
# ═══════════════════════════════════════

OPENMW_LOG = os.path.expanduser("~/Library/Preferences/openmw/openmw.log")
SCREENSHOT_DIR = os.path.expanduser("~/.timmy/morrowind/screenshots")
os.makedirs(SCREENSHOT_DIR, exist_ok=True)

# CGEvent key codes
KEYCODES = {
    "w": 13, "a": 0, "s": 1, "d": 2,
    "space": 49, "escape": 53, "return": 36,
    "e": 14, "r": 15, "f": 3, "q": 12,
    "tab": 48, "1": 18, "2": 19, "3": 20, "4": 21,
    "5": 23, "6": 22, "7": 26, "8": 28, "9": 25,
    "f5": 96, "f9": 101,  # quicksave / quickload
    "backtick": 50,  # console
    "j": 38,  # journal
    "up": 126, "down": 125, "left": 123, "right": 124,
}

# ═══════════════════════════════════════
# PERCEPTION — Parse OpenMW log
# ═══════════════════════════════════════

def parse_latest_perception():
    """Parse the most recent perception block from the OpenMW log."""
    try:
        with open(OPENMW_LOG, "r") as f:
            content = f.read()
    except FileNotFoundError:
        return {"error": "OpenMW log not found. Is the game running?"}

    # Find all perception blocks
    blocks = re.findall(
        r"=== TIMMY PERCEPTION ===\n(.*?)(?:=== END PERCEPTION ===)",
        content, re.DOTALL
    )

    if not blocks:
        return {"error": "No perception data in log. Game may not be running or Lua scripts not loaded."}

    # Parse latest block
    block = blocks[-1]
    state = {
        "npcs": [],
        "doors": [],
        "items": [],
    }

    for line in block.strip().split("\n"):
        line = line.strip()
        # Strip Lua log prefix if present
        if "]:\t" in line:
            line = line.split("]:\t", 1)[1]

        if line.startswith("Cell:"):
            state["cell"] = line.split(":", 1)[1].strip()
        elif line.startswith("Pos:"):
            state["position"] = line.split(":", 1)[1].strip()
        elif line.startswith("Yaw:"):
            state["yaw"] = line.split(":", 1)[1].strip()
        elif line.startswith("HP:"):
            state["health"] = line.split(":", 1)[1].strip()
        elif line.startswith("MP:"):
            state["magicka"] = line.split(":", 1)[1].strip()
        elif line.startswith("FT:"):
            state["fatigue"] = line.split(":", 1)[1].strip()
        elif line.startswith("Mode:"):
            state["mode"] = line.split(":", 1)[1].strip()
        elif line.startswith("Time:"):
            state["game_time"] = line.split(":", 1)[1].strip()
        elif line.startswith("NPC:"):
            state["npcs"].append(line[4:].strip())
        elif line.startswith("Door:"):
            state["doors"].append(line[5:].strip())
        elif line.startswith("Item:"):
            state["items"].append(line[5:].strip())

    return state


def get_game_status():
    """Check if OpenMW is running."""
    result = subprocess.run(["pgrep", "-f", "openmw"], capture_output=True, text=True)
    running = result.returncode == 0
    return {
        "running": running,
        "pid": result.stdout.strip().split("\n")[0] if running else None,
    }


# ═══════════════════════════════════════
# ACTIONS — CGEvent keypresses
# ═══════════════════════════════════════

def send_key(keycode, duration=0.0, shift=False):
    """Send a keypress to the game via CGEvent."""
    import Quartz

    flags = Quartz.kCGEventFlagMaskShift if shift else 0

    down = Quartz.CGEventCreateKeyboardEvent(None, keycode, True)
    Quartz.CGEventSetFlags(down, flags)
    Quartz.CGEventPost(Quartz.kCGHIDEventTap, down)

    if duration > 0:
        time.sleep(duration)

    up = Quartz.CGEventCreateKeyboardEvent(None, keycode, False)
    Quartz.CGEventSetFlags(up, 0)
    Quartz.CGEventPost(Quartz.kCGHIDEventTap, up)


def take_screenshot():
    """Take a screenshot via Quartz."""
    import Quartz
    import CoreFoundation

    image = Quartz.CGDisplayCreateImage(Quartz.CGMainDisplayID())
    if not image:
        return None

    fname = f"morrowind_{int(time.time())}.png"
    path = os.path.join(SCREENSHOT_DIR, fname)

    url = CoreFoundation.CFURLCreateWithFileSystemPath(None, path, 0, False)
    dest = Quartz.CGImageDestinationCreateWithURL(url, "public.png", 1, None)
    Quartz.CGImageDestinationAddImage(dest, image, None)
    Quartz.CGImageDestinationFinalize(dest)

    return path


# ═══════════════════════════════════════
# MCP SERVER
# ═══════════════════════════════════════

app = Server("morrowind")


@app.list_tools()
async def list_tools():
    return [
        Tool(
            name="perceive",
            description="Get Timmy's current perception of the game world: position, health, nearby NPCs, doors, items. Updates every 2 seconds from the Lua engine.",
            inputSchema={"type": "object", "properties": {}, "required": []},
        ),
        Tool(
            name="status",
            description="Check if Morrowind (OpenMW) is running.",
            inputSchema={"type": "object", "properties": {}, "required": []},
        ),
        Tool(
            name="move",
            description="Move the player character. Direction: forward, backward, left, right, turn_left, turn_right. Duration in seconds.",
            inputSchema={
                "type": "object",
                "properties": {
                    "direction": {
                        "type": "string",
                        "enum": ["forward", "backward", "left", "right", "turn_left", "turn_right"],
                        "description": "Movement direction",
                    },
                    "duration": {
                        "type": "number",
                        "description": "How long to move in seconds (default: 1.0)",
                        "default": 1.0,
                    },
                    "run": {
                        "type": "boolean",
                        "description": "Hold shift to run (default: false)",
                        "default": False,
                    },
                },
                "required": ["direction"],
            },
        ),
        Tool(
            name="action",
            description="Perform a game action: activate (use/interact with what you're looking at), jump, attack, journal, quicksave, quickload, sneak, wait.",
            inputSchema={
                "type": "object",
                "properties": {
                    "action": {
                        "type": "string",
                        "enum": ["activate", "jump", "attack", "journal", "quicksave", "quickload", "sneak", "wait"],
                        "description": "Action to perform",
                    },
                },
                "required": ["action"],
            },
        ),
        Tool(
            name="screenshot",
            description="Take a screenshot of the game. Returns the file path for vision analysis.",
            inputSchema={"type": "object", "properties": {}, "required": []},
        ),
    ]


@app.call_tool()
async def call_tool(name: str, arguments: dict):
    if name == "perceive":
        state = parse_latest_perception()
        return [TextContent(type="text", text=json.dumps(state, indent=2))]

    elif name == "status":
        status = get_game_status()
        return [TextContent(type="text", text=json.dumps(status, indent=2))]

    elif name == "move":
        direction = arguments.get("direction", "forward")
        duration = arguments.get("duration", 1.0)
        run = arguments.get("run", False)

        key_map = {
            "forward": "w",
            "backward": "s",
            "left": "a",
            "right": "d",
            "turn_left": "left",
            "turn_right": "right",
        }

        key = key_map.get(direction)
        if not key:
            return [TextContent(type="text", text=f"Unknown direction: {direction}")]

        keycode = KEYCODES[key]
        send_key(keycode, duration=duration, shift=run)
        return [TextContent(type="text", text=f"Moved {direction} for {duration}s" + (" (running)" if run else ""))]

    elif name == "action":
        action = arguments.get("action")
        action_map = {
            "activate": ("space", 0.1),
            "jump": ("space", 0.05),  # tap for jump when moving
            "attack": ("f", 0.3),  # use key
            "journal": ("j", 0.1),
            "quicksave": ("f5", 0.1),
            "quickload": ("f9", 0.1),
            "sneak": ("q", 0.1),  # toggle sneak/autowalk depending on config
            "wait": ("t", 0.1),
        }

        if action not in action_map:
            return [TextContent(type="text", text=f"Unknown action: {action}")]

        key, dur = action_map[action]
        send_key(KEYCODES[key], duration=dur)
        return [TextContent(type="text", text=f"Performed: {action}")]

    elif name == "screenshot":
        path = take_screenshot()
        if path:
            return [TextContent(type="text", text=f"Screenshot saved: {path}")]
        else:
            return [TextContent(type="text", text="Screenshot failed — display not available")]

    return [TextContent(type="text", text=f"Unknown tool: {name}")]


async def main():
    async with stdio_server() as (read_stream, write_stream):
        await app.run(
            read_stream,
            write_stream,
            app.create_initialization_options(),
        )


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