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

Body:   tmux session running terminal Angband
Eyes:   tmux capture-pane
Hands:  tmux send-keys
Brain:  Hermes TUI via MCP tools

This keeps gameplay visible, local, and telemetry-friendly.
"""

import json
import os
import shlex
import subprocess
import time
from pathlib import Path

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

ANGBAND_BIN = "/opt/homebrew/bin/angband"
ANGBAND_ROOT = Path.home() / ".timmy" / "angband"
RUNTIME_DIR = ANGBAND_ROOT / "runtime"
USER_DIR = RUNTIME_DIR / "user"
SAVE_DIR = RUNTIME_DIR / "save"
ARCHIVE_DIR = RUNTIME_DIR / "archive"
PANIC_DIR = RUNTIME_DIR / "panic"
SCORES_DIR = RUNTIME_DIR / "scores"
LOG_DIR = ANGBAND_ROOT / "logs"
SESSION_NAME = "Angband"
DEFAULT_USER = "timmy"
DEFAULT_WIDTH = 120
DEFAULT_HEIGHT = 40

app = Server("angband")


def ensure_dirs():
    for path in (ANGBAND_ROOT, RUNTIME_DIR, USER_DIR, SAVE_DIR, ARCHIVE_DIR, PANIC_DIR, SCORES_DIR, LOG_DIR):
        path.mkdir(parents=True, exist_ok=True)


def tmux(args, check=True):
    result = subprocess.run(["tmux", *args], capture_output=True, text=True)
    if check and result.returncode != 0:
        raise RuntimeError(result.stderr.strip() or result.stdout.strip() or f"tmux failed: {' '.join(args)}")
    return result


def session_exists(session_name=SESSION_NAME):
    return tmux(["has-session", "-t", session_name], check=False).returncode == 0


def pane_id(session_name=SESSION_NAME):
    if not session_exists(session_name):
        return None
    out = tmux(["list-panes", "-t", session_name, "-F", "#{pane_id}"]).stdout.strip().splitlines()
    return out[0].strip() if out else None


def capture_screen(lines=60, session_name=SESSION_NAME):
    pid = pane_id(session_name)
    if not pid:
        return "No Angband tmux pane found."
    # Angband runs in the terminal's alternate screen buffer. `-a` is required
    # or tmux returns an empty capture even while the game is visibly running.
    result = tmux(["capture-pane", "-a", "-p", "-t", pid, "-S", f"-{max(10, int(lines))}"])
    return result.stdout.rstrip()


def has_save(user=DEFAULT_USER):
    if not SAVE_DIR.exists():
        return False
    for path in SAVE_DIR.iterdir():
        if path.name.startswith(user):
            return True
    return False


SPECIAL_KEYS = {
    "enter": "Enter",
    "return": "Enter",
    "esc": "Escape",
    "escape": "Escape",
    "up": "Up",
    "down": "Down",
    "left": "Left",
    "right": "Right",
    "space": "Space",
    "tab": "Tab",
    "backspace": "BSpace",
    "delete": "DC",
    "home": "Home",
    "end": "End",
    "pageup": "PageUp",
    "pagedown": "PageDown",
    "pgup": "PageUp",
    "pgdn": "PageDown",
    "ctrl-c": "C-c",
    "ctrl-x": "C-x",
    "ctrl-z": "C-z",
}


def send_key(key, session_name=SESSION_NAME):
    pid = pane_id(session_name)
    if not pid:
        raise RuntimeError("No Angband tmux pane found.")
    normalized = str(key).strip()
    mapped = SPECIAL_KEYS.get(normalized.lower())
    if mapped:
        tmux(["send-keys", "-t", pid, mapped])
    elif len(normalized) == 1:
        tmux(["send-keys", "-t", pid, "-l", normalized])
    else:
        # Let tmux interpret names like F1 if passed through.
        tmux(["send-keys", "-t", pid, normalized])


def send_text(text, session_name=SESSION_NAME):
    pid = pane_id(session_name)
    if not pid:
        raise RuntimeError("No Angband tmux pane found.")
    tmux(["send-keys", "-t", pid, "-l", text])


def maybe_continue_splash(session_name=SESSION_NAME):
    screen = capture_screen(80, session_name)
    advanced = False
    if "Press any key to continue" in screen:
        send_key("enter", session_name)
        time.sleep(0.8)
        screen = capture_screen(80, session_name)
        advanced = True
    return advanced, screen


def launch_game(user=DEFAULT_USER, new_game=False, continue_splash=True, width=DEFAULT_WIDTH, height=DEFAULT_HEIGHT):
    ensure_dirs()

    if not Path(ANGBAND_BIN).exists():
        return {
            "error": f"Angband binary not found: {ANGBAND_BIN}"
        }

    if session_exists():
        advanced = False
        screen = capture_screen(80)
        if continue_splash:
            advanced, screen = maybe_continue_splash()
        return {
            "launched": False,
            "already_running": True,
            "session": SESSION_NAME,
            "attach": f"tmux attach -t {SESSION_NAME}",
            "continued_splash": advanced,
            "screen": screen,
        }

    use_new_game = bool(new_game or not has_save(user))
    cmd = [
        ANGBAND_BIN,
        f"-u{user}",
        "-mgcu",
        f"-duser={USER_DIR}",
        f"-dsave={SAVE_DIR}",
        f"-darchive={ARCHIVE_DIR}",
        f"-dpanic={PANIC_DIR}",
        f"-dscores={SCORES_DIR}",
    ]
    if use_new_game:
        cmd.insert(1, "-n")

    shell_cmd = "export TERM=xterm-256color; exec " + " ".join(shlex.quote(part) for part in cmd)
    tmux([
        "new-session", "-d",
        "-s", SESSION_NAME,
        "-x", str(int(width)),
        "-y", str(int(height)),
        shell_cmd,
    ])

    time.sleep(2.5)
    advanced = False
    screen = capture_screen(80)
    if continue_splash:
        advanced, screen = maybe_continue_splash()

    return {
        "launched": True,
        "already_running": False,
        "new_game": use_new_game,
        "session": SESSION_NAME,
        "attach": f"tmux attach -t {SESSION_NAME}",
        "continued_splash": advanced,
        "screen": screen,
    }


def stop_game():
    if not session_exists():
        return {"stopped": False, "message": "Angband session is not running."}
    tmux(["kill-session", "-t", SESSION_NAME])
    return {"stopped": True, "session": SESSION_NAME}


def status():
    running = session_exists()
    savefiles = []
    if SAVE_DIR.exists():
        savefiles = sorted(path.name for path in SAVE_DIR.iterdir())
    result = {
        "running": running,
        "session": SESSION_NAME if running else None,
        "attach": f"tmux attach -t {SESSION_NAME}" if running else None,
        "savefiles": savefiles,
    }
    if running:
        result["screen"] = capture_screen(40)
    return result


def observe(lines=60):
    return {
        "running": session_exists(),
        "session": SESSION_NAME if session_exists() else None,
        "screen": capture_screen(lines),
    }


def keypress(key, wait_ms=500):
    send_key(key)
    time.sleep(max(0, int(wait_ms)) / 1000.0)
    return {
        "sent": key,
        "screen": capture_screen(60),
    }


def type_and_observe(text, wait_ms=500):
    send_text(text)
    time.sleep(max(0, int(wait_ms)) / 1000.0)
    return {
        "sent": text,
        "screen": capture_screen(60),
    }


@app.list_tools()
async def list_tools():
    return [
        Tool(
            name="status",
            description="Check whether the watchable Angband tmux session is running, list savefiles, and return the current visible screen when available.",
            inputSchema={"type": "object", "properties": {}, "required": []},
        ),
        Tool(
            name="launch",
            description="Launch terminal Angband inside a watchable tmux session named Angband. Loads an existing save for the given user when present; otherwise starts a new game. Can auto-advance the initial splash screen.",
            inputSchema={
                "type": "object",
                "properties": {
                    "user": {"type": "string", "description": "Savefile/user slot name (default: timmy)"},
                    "new_game": {"type": "boolean", "description": "Force a new game even if a save exists.", "default": False},
                    "continue_splash": {"type": "boolean", "description": "Press Enter automatically if the splash page says 'Press any key to continue'.", "default": True},
                    "width": {"type": "integer", "description": "tmux width for the visible game session", "default": 120},
                    "height": {"type": "integer", "description": "tmux height for the visible game session", "default": 40},
                },
                "required": [],
            },
        ),
        Tool(
            name="observe",
            description="Read the current Angband screen as plain text from the tmux pane. Use this before acting.",
            inputSchema={
                "type": "object",
                "properties": {
                    "lines": {"type": "integer", "description": "How many recent screen lines to capture", "default": 60},
                },
                "required": [],
            },
        ),
        Tool(
            name="keypress",
            description="Send one key to Angband and then return the updated screen. Common keys: Enter, Escape, Up, Down, Left, Right, Space, Tab, Backspace, ctrl-x, ?, *, @, letters, numbers.",
            inputSchema={
                "type": "object",
                "properties": {
                    "key": {"type": "string", "description": "Key to send"},
                    "wait_ms": {"type": "integer", "description": "Milliseconds to wait before recapturing the screen", "default": 500},
                },
                "required": ["key"],
            },
        ),
        Tool(
            name="type_text",
            description="Type literal text into Angband and then return the updated screen. Useful when a menu expects a name or command string.",
            inputSchema={
                "type": "object",
                "properties": {
                    "text": {"type": "string", "description": "Literal text to type"},
                    "wait_ms": {"type": "integer", "description": "Milliseconds to wait before recapturing the screen", "default": 500},
                },
                "required": ["text"],
            },
        ),
        Tool(
            name="stop",
            description="Kill the watchable Angband tmux session.",
            inputSchema={"type": "object", "properties": {}, "required": []},
        ),
    ]


@app.call_tool()
async def call_tool(name: str, arguments: dict):
    arguments = arguments or {}

    if name == "status":
        result = status()
    elif name == "launch":
        result = launch_game(
            user=arguments.get("user", DEFAULT_USER),
            new_game=arguments.get("new_game", False),
            continue_splash=arguments.get("continue_splash", True),
            width=arguments.get("width", DEFAULT_WIDTH),
            height=arguments.get("height", DEFAULT_HEIGHT),
        )
    elif name == "observe":
        result = observe(lines=arguments.get("lines", 60))
    elif name == "keypress":
        result = keypress(arguments.get("key", ""), wait_ms=arguments.get("wait_ms", 500))
    elif name == "type_text":
        result = type_and_observe(arguments.get("text", ""), wait_ms=arguments.get("wait_ms", 500))
    elif name == "stop":
        result = stop_game()
    else:
        result = {"error": f"Unknown tool: {name}"}

    return [TextContent(type="text", text=json.dumps(result, indent=2))]


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())
