#!/usr/bin/env python3
"""Golem MCP — Bridge between Claude Code and Godot 4.x via FastMCP."""

import asyncio
import atexit
import base64
import logging
import os
import sys
import tempfile
from pathlib import Path
from mcp.server.fastmcp import FastMCP, Image
from golem_client import GolemClient

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("golem_mcp")

mcp = FastMCP(
    "godot_mcp",
    instructions=(
        "Golem MCP — live bridge to a running Godot 4.x editor via persistent TCP connection. "
        "Tools manipulate the EDITOR state (scene tree, properties, scripts), not the running game. "
        "Key workflow: open_scene → add_nodes/update_property → save_scene → play_scene → get_logs/game_screenshot → stop_scene. "
        "IMPORTANT: always call godot_save_scene after scene modifications (add/delete/update nodes). "
        "Never edit .tscn files on disk — use the scene manipulation tools instead. "
        "For game runtime data, use get_logs (requires GolemCapture.log() in scripts) and get_game_screenshot."
    ),
)
client = GolemClient()


# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------

async def _ensure_connected():
    await client.ensure_connected()


async def _call(tool: str, args: dict | None = None, timeout: float | None = None) -> str | Image:
    """Call Godot and return the appropriate MCP content."""
    from golem_license import is_licensed, is_allowed, increment_trial_usage, get_trial_status

    # Check license/trial status before each call
    allowed, mode = is_allowed()
    if not allowed:
        return "Error: Trial expired. Please activate a license: golem-mcp activate <key>"

    # Increment trial usage if not licensed
    if mode == "trial":
        status = increment_trial_usage()
        remaining = status["remaining"]
        if remaining <= 3 and remaining > 0:
            logger.warning(f"Trial: {remaining} calls remaining")

    await _ensure_connected()
    result = await client.call(tool, args or {}, timeout=timeout)

    if result.get("type") == "image":
        image_data = base64.b64decode(result["result"])
        return Image(data=image_data, format=result.get("mime", "image/png"))

    return result.get("result", "")


async def _call_with_retry(
    tool: str,
    args: dict | None = None,
    timeout: float | None = None,
    retries: int = 2,
    delay: float = 1.0,
) -> str | Image:
    """Call Godot with automatic retry on transient failures (ConnectionError, TimeoutError)."""
    last_error = None
    for attempt in range(retries + 1):
        try:
            return await _call(tool, args, timeout)
        except (ConnectionError, TimeoutError) as exc:
            last_error = exc
            if attempt < retries:
                logger.info("Retry %d/%d for '%s' after %s", attempt + 1, retries, tool, type(exc).__name__)
                await asyncio.sleep(delay)
    raise last_error


# ==================================================================
# Phase 1 — Core tools
# ==================================================================

@mcp.tool(annotations={"readOnlyHint": True})
async def godot_get_scene_tree(detail_level: str = "full") -> str:
    """Get the full node hierarchy of the currently edited scene.

    Use this to understand scene structure, verify nodes after add/delete operations, or plan modifications.
    For detailed properties of a single node, use godot_inspect_node instead.
    For a broad project overview (settings, autoloads, file lists), use godot_project_context instead.

    Returns the tree with indentation showing parent-child relationships.
    Safe to call while a game is running (reads the editor scene, not the running game).

    Args:
        detail_level: "full" (default) includes script paths on each node. "summary" shows only names and types (faster for large scenes).
    """
    return await _call_with_retry("get_scene_tree", {"detail_level": detail_level})


@mcp.tool(annotations={"readOnlyHint": True})
async def godot_inspect_node(path: str, detail_level: str = "full") -> str:
    """Get all properties, signals, and groups of a specific node in the edited scene.

    Use this when you need to read or verify property values on a known node.
    Use godot_get_scene_tree first if you don't know the node path.

    Args:
        path: Node path relative to scene root. Use "." for the root node, "Player" for a direct child, "Player/Sprite" for nested nodes.
        detail_level: "full" (default) shows all editor-visible properties, signals with connections, and groups. "summary" shows only type, path, script, and transform properties (position, rotation, scale, size, visible).
    """
    return await _call("inspect_node", {"path": path, "detail_level": detail_level})


@mcp.tool(annotations={"readOnlyHint": True})
async def godot_get_editor_screenshot() -> Image:
    """Capture a screenshot of the Godot editor's 2D/3D viewport (not the running game).

    Use this to see the current scene layout, verify visual changes after node operations, or audit the design.
    For screenshots of the running game, use godot_get_game_screenshot instead (requires the game to be playing).
    """
    return await _call("get_editor_screenshot")


@mcp.tool(annotations={"readOnlyHint": True})
async def godot_get_game_screenshot() -> Image:
    """Capture a screenshot of the currently running game window.

    REQUIREMENTS: The game must be playing (call godot_play_scene first) and the GolemCapture autoload
    must be active in the project (added automatically by the Golem installer).
    Uses a file-based protocol — GolemCapture in the game process captures and writes the screenshot,
    then the editor plugin reads it. This may take 1-2 seconds.

    WARNING: May fail right after play_scene due to TCP reconnect timing. Built-in retry handles this.
    For editor viewport screenshots (no game running), use godot_get_editor_screenshot instead.
    """
    return await _call_with_retry("get_game_screenshot", timeout=10.0)


@mcp.tool()
async def godot_play_scene(path: str = "") -> str:
    """Start playing a scene in the Godot editor (equivalent to pressing F5/F6).

    Only one scene can run at a time. Call godot_stop_scene first if a scene is already running.
    After starting, use godot_get_logs and godot_get_game_screenshot to observe the game.
    Save your scene (godot_save_scene) before playing to ensure changes are included.

    Args:
        path: Scene file path (e.g. "res://scenes/main.tscn"). Empty string plays the current scene. If no main scene is set and path is empty, plays the currently edited scene.
    """
    return await _call("play_scene", {"path": path})


@mcp.tool()
async def godot_stop_scene() -> str:
    """Stop the currently running game scene.

    Waits until Godot has fully torn down the game process before returning (deferred).
    This means you can safely call godot_play_scene immediately after without getting
    "A scene is already running" errors.

    Typical workflow: stop_scene → modify scene/scripts → save_scene → play_scene.
    """
    return await _call("stop_scene")


@mcp.tool(annotations={"readOnlyHint": True})
async def godot_get_errors() -> str:
    """Get GDScript compilation errors and runtime errors captured by the editor.

    Returns errors accumulated since the last clear_logs call. Includes both parse/compile
    errors (from script saves) and runtime errors (from the running game).
    Reliable even right after play_scene (unlike screenshots, this rarely fails).
    """
    return await _call("get_errors")


@mcp.tool()
async def godot_clear_logs() -> str:
    """Clear all error records, game log files, and engine log cursors.

    Resets: the error buffer (get_errors), the game log file (GolemCapture logs),
    the engine log read position, and input simulation files.
    Call this before a test run to get a clean slate.
    Note: play_scene also resets game logs automatically.
    """
    return await _call("clear_logs")


@mcp.tool()
async def godot_execute_script(code: str) -> str:
    """Execute GDScript code in the EDITOR context (not the running game) and return the result.

    The code is wrapped in a function body and compiled at runtime. Use 'return <value>' to get a result back.
    Best for: assigning Resources (load()), setting SpriteFrames, enabling unique_name_in_owner, reading data.

    CRITICAL LIMITATIONS:
    - NO await allowed (editor @tool context). Use godot_write_script for async code.
    - Runs in EDITOR, not the game. Cannot access game autoloads (GameState, etc.).
    - Keep scripts simple — complex arrays + loops + string concat may fail (error code 36).
      For complex logic, write a .gd file with godot_write_script instead.
    - Results must be JSON-serializable. Return strings, numbers, arrays, dicts.

    Args:
        code: GDScript code body. Example: 'var node = get_node("/root/Main/Sprite")\\nnode.texture = load("res://icon.png")\\nreturn "done"'
    """
    return await _call("execute_script", {"code": code})


@mcp.tool(annotations={"readOnlyHint": True})
async def godot_view_script(path: str) -> str:
    """Read the content of a GDScript (.gd) or shader (.gdshader) file from the Godot project.

    Resolves res:// paths through Godot, so it works even if the project is in a non-standard location.
    For writing scripts, use godot_write_script instead.

    Args:
        path: Script file path (e.g. "res://scripts/player.gd", "res://shaders/outline.gdshader").
    """
    return await _call("view_script", {"path": path})


@mcp.tool()
async def godot_write_script(path: str, content: str) -> str:
    """Write or overwrite a GDScript file in the project. Creates directories if needed.

    Godot auto-reloads scripts on save, so changes take effect immediately in the editor.
    After writing, attach the script to a node with godot_attach_script if it's a new file.

    Args:
        path: File path (e.g. "res://scripts/player.gd"). Auto-prefixed with res:// if missing.
        content: Full GDScript source code to write. Must include the complete file content (not a diff).
    """
    return await _call("write_script", {"path": path, "content": content})


@mcp.tool()
async def godot_attach_script(node: str, script: str) -> str:
    """Attach an existing GDScript file to a node in the currently edited scene.

    The script must already exist on disk (use godot_write_script first).
    Call godot_save_scene after to persist the attachment.

    Args:
        node: Node path relative to scene root (e.g. "Player", "UI/HUD"). Use "." for the root node.
        script: Script file path (e.g. "res://scripts/player.gd"). Must be a valid res:// path.
    """
    return await _call("attach_script", {"node": node, "script": script})


@mcp.tool(annotations={"readOnlyHint": True})
async def godot_get_logs(max_lines: int = 100, source: str = "game") -> str:
    """Get logs from the running game or the Godot engine. INCREMENTAL: each call returns only NEW lines since the last call.

    Game logs require GolemCapture.log("msg") in your scripts — standard print() goes to stdout and is NOT captured.
    Engine logs come from Godot's godot.log file.
    Use godot_clear_logs to reset read positions. godot_play_scene also resets game logs.

    Args:
        max_lines: Maximum number of log lines to return. Default 100.
        source: "game" for GolemCapture output, "engine" for Godot engine logs, "all" for both. Default "game".
    """
    return await _call("get_logs", {"max_lines": max_lines, "source": source})


@mcp.tool()
async def godot_simulate_input(sequence: list, loop: int = 1, timeout: float = 30.0) -> str:
    """Simulate input actions in the running game via GolemCapture (file-based protocol).

    The game must be playing (call godot_play_scene first). Sends a sequence of input steps
    that GolemCapture executes in order. Only works with named Input Map actions — does NOT
    simulate raw mouse/keyboard events.

    Step types:
      - {"action": "move_right", "duration": 2.0} — press, hold N sec, release
      - {"wait": 0.5} — pause N seconds between steps
      - {"action": "jump", "pressed": true} — toggle on (no auto-release)
      - {"action": "jump", "pressed": false} — toggle off

    Workflow: play_scene → simulate_input → get_logs / get_game_screenshot → stop_scene.

    Args:
        sequence: Array of step dicts. Each must have "action" (with optional "duration" or "pressed") or "wait".
        loop: Number of times to repeat the full sequence. Default 1.
        timeout: Max wait time in seconds for the full sequence to complete. Default 30.
    """
    return await _call("simulate_input", {"sequence": sequence, "loop": loop}, timeout=timeout)


# ==================================================================
# Phase 2 — Scene manipulation
# ==================================================================

@mcp.tool()
async def godot_add_node(
    type: str = "Node",
    name: str = "",
    parent: str = "",
) -> str:
    """Add a single new node to the currently edited scene.

    For creating multiple nodes at once (with hierarchy and properties), use godot_add_nodes instead.
    This tool creates a bare node with no properties set — use godot_update_property after.
    IMPORTANT: call godot_save_scene after adding nodes to persist changes to disk.

    Args:
        type: Godot node class name (e.g. "Sprite2D", "CharacterBody2D", "Label", "Area2D").
        name: Name for the new node. Auto-generated from type if empty.
        parent: Path to parent node relative to scene root (e.g. "Player", "UI/HUD"). Empty = scene root.
    """
    return await _call("add_node", {"type": type, "name": name, "parent": parent})


@mcp.tool()
async def godot_delete_node(path: str) -> str:
    """Delete a node and all its children from the currently edited scene.

    Cannot delete the scene root node. Call godot_save_scene after to persist the change.

    Args:
        path: Node path relative to scene root (e.g. "Player", "UI/OldButton"). Cannot be ".".
    """
    return await _call("delete_node", {"path": path})


@mcp.tool()
async def godot_update_property(path: str, property: str, value) -> str:
    """Set a property value on a node in the currently edited scene.

    Supports automatic type conversion: arrays → Vector2/Vector3/Rect2/Color, strings → Color ("#ff0000"),
    res:// strings → loaded Resources, dicts with "type" → new Resource instances.
    If type conversion fails (e.g. complex Resources like SpriteFrames), use godot_execute_script with load() instead.
    IMPORTANT: call godot_save_scene after to persist changes.

    Args:
        path: Node path relative to scene root. Use "." for the root node.
        property: Property name (e.g. "position", "modulate", "text", "texture", "visible").
        value: New value. Type conversion examples:
            - Vector2: [100, 200]
            - Vector3: [1, 2, 3]
            - Color: "#ff0000" or [1.0, 0.0, 0.0, 1.0]
            - Resource: "res://icon.png" (loaded automatically)
            - Resource from spec: {"type": "RectangleShape2D", "properties": {"size": [32, 32]}}
    """
    return await _call("update_property", {"path": path, "property": property, "value": value})


@mcp.tool()
async def godot_move_node(path: str, new_parent: str) -> str:
    """Reparent a node (and its children) to a different parent in the scene tree.

    Call godot_save_scene after to persist.

    Args:
        path: Current node path relative to scene root.
        new_parent: Path to the new parent node relative to scene root.
    """
    return await _call("move_node", {"path": path, "new_parent": new_parent})


@mcp.tool()
async def godot_duplicate_node(path: str) -> str:
    """Duplicate a node and all its children as a sibling in the scene tree.

    The duplicate gets an auto-incremented name (e.g. "Sprite2" if "Sprite" is duplicated).
    Call godot_save_scene after to persist.

    Args:
        path: Node path to duplicate relative to scene root.
    """
    return await _call("duplicate_node", {"path": path})


@mcp.tool()
async def godot_add_nodes(nodes: list) -> str:
    """Create an entire node hierarchy in one call (batch). Preferred over multiple add_node calls.

    Supports nested children and property assignment with automatic type conversion (Vector2, Color, etc.).
    IMPORTANT: Resource properties (Texture, SpriteFrames, Material, Theme) passed as res:// strings will
    return an error if the resource file doesn't exist. For complex Resources, use godot_execute_script
    with load() after creating nodes.
    IMPORTANT: call godot_save_scene after to persist the new nodes.

    Args:
        nodes: Array of node specs. Each spec is a dict with:
            - name: Node name (required)
            - type: Godot class (e.g. "Sprite2D", "CharacterBody2D"). Default "Node"
            - parent: Parent path relative to scene root (optional, default = scene root)
            - properties: Dict of property values to set (optional). Same conversion rules as update_property.
            - children: Array of child node specs (optional, recursive)

    Example:
        [{"name": "Player", "type": "CharacterBody2D", "properties": {"position": [100, 200]}, "children": [
            {"name": "Sprite", "type": "Sprite2D"},
            {"name": "Collision", "type": "CollisionShape2D", "properties": {"shape": {"type": "RectangleShape2D", "properties": {"size": [32, 64]}}}}
        ]}]
    """
    return await _call("add_nodes", {"nodes": nodes})


@mcp.tool()
async def godot_create_scene(
    path: str,
    root_type: str = "Node2D",
    root_name: str = "",
) -> str:
    """Create a new .tscn scene file, save it to disk, and open it in the editor.

    Creates parent directories if they don't exist. After creation, the scene is the active edited scene
    and you can immediately add nodes to it.

    Args:
        path: Scene file path (e.g. "res://scenes/main.tscn"). Auto-adds res:// prefix and .tscn extension if missing.
        root_type: Root node class (e.g. "Node2D", "Node3D", "Control", "CharacterBody2D"). Default "Node2D".
        root_name: Name for root node. Default = derived from filename.
    """
    return await _call("create_scene", {"path": path, "root_type": root_type, "root_name": root_name})


@mcp.tool()
async def godot_open_scene(path: str) -> str:
    """Open an existing .tscn scene file in the editor, making it the active edited scene.

    If a game is currently running, it will NOT be stopped — but the edited scene context changes.
    Call godot_stop_scene first if you need to stop the game before switching scenes.
    After opening, use godot_get_scene_tree to see the scene's node hierarchy.

    Args:
        path: Scene file path (e.g. "res://scenes/main.tscn"). Auto-prefixed with res:// if missing.
    """
    return await _call("open_scene", {"path": path})


@mcp.tool(annotations={"readOnlyHint": True})
async def godot_find_nodes(
    type: str = "",
    group: str = "",
    name: str = "",
    script: str = "",
    max_results: int = 50,
) -> str:
    """Search the edited scene tree for nodes matching one or more filters. At least one filter required.

    Useful to find nodes by type ("all Area2D nodes"), by group, by name pattern, or by attached script.
    Returns node paths, types, script paths, and groups for each match.

    Args:
        type: Godot class name (e.g. "Label", "Area2D"). Matches subclasses too (e.g. "Control" matches Button, Label, etc.).
        group: Group name (e.g. "enemies", "saveable").
        name: Name pattern with optional * wildcards (e.g. "Score*", "*Button", "*Health*"). Case-insensitive.
        script: Script filename suffix (e.g. "player.gd", "ui/hud.gd"). Matches end of script path.
        max_results: Maximum nodes to return. Default 50.
    """
    args = {"max_results": max_results}
    if type:
        args["type"] = type
    if group:
        args["group"] = group
    if name:
        args["name"] = name
    if script:
        args["script"] = script
    return await _call("find_nodes", args)


# ==================================================================
# Phase 3 — Advanced
# ==================================================================

@mcp.tool(annotations={"readOnlyHint": True})
async def godot_get_input_map() -> str:
    """Get all custom input actions defined in the project and their key/button bindings.

    Returns only actions defined in Project Settings > Input Map (not built-in Godot actions).
    NOTE: this reads from the editor's Input Map, which may not include actions added at runtime.
    For the raw [input] section, read project.godot directly.
    """
    return await _call("get_input_map")


@mcp.tool()
async def godot_project_settings(
    action: str = "list",
    key: str = "",
    value=None,
) -> str:
    """Read or modify Godot project settings (project.godot). Changes are persisted by Godot automatically.

    IMPORTANT: always use the full path format for keys: "display/window/size/viewport_width"
    (not the .ini section format "window/size/viewport_width" under [display]).
    Verify changes with action="get" after setting.

    Args:
        action: "list" to show common settings, "get" to read a specific setting, "set" to modify one.
        key: Full setting key path (e.g. "application/config/name", "display/window/size/viewport_width"). Required for get/set.
        value: New value (only for action="set"). Type must match the setting's expected type.
    """
    args = {"action": action, "key": key}
    if value is not None:
        args["value"] = value
    return await _call("project_settings", args)


@mcp.tool()
async def godot_save_scene() -> str:
    """Save the currently edited scene to its .tscn file on disk.

    REQUIRED after any scene modification: add_node(s), delete_node, update_property, move_node,
    duplicate_node, attach_script, or execute_script that modifies the scene tree.
    Without saving, changes exist only in editor memory and will be lost.
    """
    return await _call("save_scene")


@mcp.tool(annotations={"readOnlyHint": True})
async def godot_project_context() -> str:
    """Get a comprehensive project overview in one call — ideal as a first tool call to understand the project.

    Returns: project name, viewport size, renderer, autoloads, custom input actions,
    and lists of all .gd/.tscn/.tres files (excluding addons/).
    Use this instead of multiple project_settings + get_input_map calls.
    For the scene tree of the currently open scene, use godot_get_scene_tree instead.
    """
    return await _call("project_context")


# ------------------------------------------------------------------
# Entry point
# ------------------------------------------------------------------

# ANSI color codes
class Colors:
    CYAN = "\033[96m"      # Bright cyan (vert d'eau)
    TEAL = "\033[38;5;80m" # True teal (256 color)
    DIM = "\033[2m"
    RESET = "\033[0m"
    BOLD = "\033[1m"


GOLEM_BANNER_RAW = """\
   █████████     ███████    █████       ██████████ ██████   ██████
  ███░░░░░███  ███░░░░░███ ░░███       ░░███░░░░░█░░██████ ██████
 ███     ░░░  ███     ░░███ ░███        ░███  █ ░  ░███░█████░███
░███         ░███      ░███ ░███        ░██████    ░███░░███ ░███
░███    █████░███      ░███ ░███        ░███░░█    ░███ ░░░  ░███
░░███  ░░███ ░░███     ███  ░███      █ ░███ ░   █ ░███      ░███
 ░░█████████  ░░░███████░   ███████████ ██████████ █████     █████
  ░░░░░░░░░     ░░░░░░░    ░░░░░░░░░░░ ░░░░░░░░░░ ░░░░░     ░░░░░\
"""


def _supports_color() -> bool:
    """Check if terminal supports colors."""
    import sys
    import os
    # Disable colors if NO_COLOR env is set or not a TTY
    if os.environ.get("NO_COLOR"):
        return False
    if not hasattr(sys.stdout, "isatty"):
        return False
    return sys.stdout.isatty()


def _detect_installed_agents() -> str:
    """Detect which AI assistants are installed and return a label."""
    import shutil
    import sys
    from pathlib import Path

    agents = []

    if shutil.which("claude"):
        agents.append("Claude Code")

    # Cursor
    if sys.platform == "darwin" and Path("/Applications/Cursor.app").exists():
        agents.append("Cursor")
    elif shutil.which("cursor"):
        agents.append("Cursor")

    # Windsurf
    if (Path.home() / ".codeium" / "windsurf").exists():
        agents.append("Windsurf")

    if not agents:
        return "AI Assistants"
    elif len(agents) == 1:
        return agents[0]
    elif len(agents) == 2:
        return f"{agents[0]} & {agents[1]}"
    else:
        return f"{agents[0]} + {len(agents) - 1} more"


def _print_banner(agent: str | None = None):
    """Print the Golem ASCII banner with optional agent tagline."""
    use_color = _supports_color()

    if use_color:
        c = Colors.TEAL
        r = Colors.RESET
        d = Colors.DIM
    else:
        c = r = d = ""

    print(f"{c}{GOLEM_BANNER_RAW}{r}")

    # Auto-detect agents if not provided
    if agent is None:
        agent = _detect_installed_agents()

    # Dynamic tagline centered under the banner
    tagline = f"Godot 4.x <-> {agent}"
    padding = (67 - len(tagline)) // 2  # 67 is roughly the banner width
    print(f"{d}{' ' * padding}{tagline}{r}")
    print()


def _print_unlicensed_message():
    """Print message when no valid license is found."""
    _print_banner()
    print("  Golem MCP requires a valid license to run.")
    print()
    print("  → Purchase: https://gumroad.com/l/golem-mcp")
    print("  → Activate: golem-mcp activate <YOUR-LICENSE-KEY>")
    print()
    print("  Trial expired? You had 10 free MCP calls.")
    print()


def _run_activate(argv: list[str]) -> None:
    """Handle the activate command."""
    from golem_license import activate, is_licensed

    if not argv:
        print("Usage: golem-mcp activate <license-key>")
        print()
        print("Example: golem-mcp activate GOLEM1-GUM-12345-A3F2B1C9")
        return

    key = argv[0]
    success, message = activate(key)

    if success:
        print(f"✓ {message}")
        print()
        print("You can now use Golem MCP. Next steps:")
        print("  1. golem-mcp install <your-godot-project>")
        print("  2. Enable plugin in Godot > Project Settings > Plugins")
    else:
        print(f"✗ {message}")
        print()
        print("Please check your license key and try again.")
        print("If you need a license: https://gumroad.com/l/golem-mcp")


def _run_status() -> None:
    """Show license status."""
    from golem_license import is_licensed, load_license, get_license_path, get_trial_status

    _print_banner()
    print("  License Status")
    print("  " + "─" * 50)

    key = load_license()
    if key:
        # Mask the key for display
        if len(key) > 20:
            masked = key[:12] + "..." + key[-8:]
        else:
            masked = key
        print(f"    Key file:   {get_license_path()}")
        print(f"    Key:        {masked}")
        print(f"    Status:     {'✓ Valid' if is_licensed() else '✗ Invalid'}")
    else:
        trial = get_trial_status()
        print(f"    Key file:   {get_license_path()}")
        print("    License:    Not installed")
        print(f"    Trial:      {trial['remaining']}/{trial['limit']} calls remaining")
        if trial['remaining'] == 0:
            print("    Status:     ✗ Trial expired")
        else:
            print("    Status:     Trial mode")
        print()
        print("    → Purchase: https://gumroad.com/l/golem-mcp")
    print()


def _run_admin(argv: list[str]) -> None:
    """Handle admin commands (requires GOLEM_ADMIN env var)."""
    import os

    if not os.environ.get("GOLEM_ADMIN"):
        print("✗ Admin commands require GOLEM_ADMIN environment variable")
        return

    if not argv:
        print("Admin commands:")
        print("  golem-mcp admin keygen <order_id>   Generate a license key")
        print("  golem-mcp admin verify <key>        Verify a license key")
        print("  golem-mcp admin bulk <file>         Generate keys from order IDs file")
        return

    subcmd = argv[0]

    if subcmd == "keygen":
        if len(argv) < 2:
            print("Usage: golem-mcp admin keygen <order_id>")
            print("Example: golem-mcp admin keygen GUM-abc123")
            return
        from golem_license import generate_key
        order_id = argv[1]
        key = generate_key(order_id)
        print(f"Order ID: {order_id}")
        print(f"Key:      {key}")

    elif subcmd == "verify":
        if len(argv) < 2:
            print("Usage: golem-mcp admin verify <key>")
            return
        from golem_license import verify_key
        key = argv[1]
        valid = verify_key(key)
        print(f"Key:    {key}")
        print(f"Valid:  {'✓ Yes' if valid else '✗ No'}")

    elif subcmd == "bulk":
        if len(argv) < 2:
            print("Usage: golem-mcp admin bulk <file>")
            print("File format: one order ID per line")
            return
        from golem_license import generate_key
        from pathlib import Path
        filepath = Path(argv[1])
        if not filepath.exists():
            print(f"✗ File not found: {filepath}")
            return
        order_ids = filepath.read_text().strip().splitlines()
        print(f"Generating {len(order_ids)} keys...")
        print()
        for order_id in order_ids:
            order_id = order_id.strip()
            if order_id:
                key = generate_key(order_id)
                print(f"{order_id},{key}")

    elif subcmd == "reset-trial":
        from golem_license import reset_trial, get_trial_status
        reset_trial()
        status = get_trial_status()
        print(f"✓ Trial reset: {status['remaining']}/{status['limit']} calls available")

    else:
        print(f"✗ Unknown admin command: {subcmd}")
        print("  Available: keygen, verify, bulk, reset-trial")


def main():
    import sys
    import io

    # Fix Windows encoding issues
    if sys.platform == "win32" and hasattr(sys.stdout, "buffer"):
        sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")

    from golem_license import is_allowed

    # Commands that don't require a license
    if len(sys.argv) > 1:
        cmd = sys.argv[1]

        if cmd == "activate":
            _run_activate(sys.argv[2:])
            return

        if cmd == "status":
            _run_status()
            return

        if cmd == "admin":
            _run_admin(sys.argv[2:])
            return

        if cmd == "install":
            # License or trial check for install
            allowed, mode = is_allowed()
            if not allowed:
                _print_unlicensed_message()
                sys.exit(1)
            from golem_installer import run_install
            run_install(sys.argv[2:])
            return

        if cmd == "uninstall":
            # License or trial check for uninstall
            allowed, mode = is_allowed()
            if not allowed:
                _print_unlicensed_message()
                sys.exit(1)
            from golem_installer import run_uninstall
            run_uninstall(sys.argv[2:])
            return

        if cmd in ("--help", "-h", "help"):
            _print_banner()
            print("  Bridge between Claude Code and Godot 4.x")
            print()
            print("  Usage:")
            print("    golem-mcp                     Run the MCP server")
            print("    golem-mcp install <project>   Install in a Godot project")
            print("    golem-mcp uninstall <project> Remove from a project")
            print("    golem-mcp activate <key>      Activate a license key")
            print("    golem-mcp status              Show license status")
            print()
            print("  Install options:")
            print("    --agent <name>   claude, cursor, windsurf, continue, cline")
            print("    --force          Overwrite existing installation")
            print("    --local <path>   Use local dev server instead of uvx")
            print("    --skip-skills    Don't copy .claude/commands/")
            print()
            print("  Trial: 10 free MCP calls. Use 'golem-mcp status' to check.")
            print()
            return

        if cmd == "--version":
            from golem_installer import _get_version
            print(f"Golem MCP v{_get_version()}")
            return

    # Running the MCP server requires a license or trial
    allowed, mode = is_allowed()
    if not allowed:
        _print_unlicensed_message()
        sys.exit(1)

    # -- P0: Lock file to prevent multiple instances --
    lock_path = Path(tempfile.gettempdir()) / "golem-mcp.lock"
    if lock_path.exists():
        try:
            old_pid = int(lock_path.read_text().strip())
            # Check if process is still alive
            try:
                os.kill(old_pid, 0)  # signal 0 = check existence
                logger.error(
                    f"Another Golem MCP instance is already running (PID {old_pid}). "
                    f"Kill it first: taskkill /PID {old_pid} /F (Windows) or kill {old_pid} (Unix)"
                )
                sys.exit(1)
            except OSError:
                # Process is dead, stale lock file — overwrite
                pass
        except (ValueError, FileNotFoundError):
            pass  # Corrupted lock file, overwrite

    lock_path.write_text(str(os.getpid()))
    atexit.register(lambda: lock_path.unlink(missing_ok=True))

    if mode == "trial":
        from golem_license import get_trial_status
        status = get_trial_status()
        logger.info(f"Running in trial mode: {status['remaining']}/{status['limit']} calls remaining")

    mcp.run()


if __name__ == "__main__":
    main()
