"""FastAPI application for the OpenRA-RL environment.

Creates the OpenEnv-compatible server using create_app().
"""

import asyncio
import json
import os
import time

from fastapi import Query
from fastapi.responses import HTMLResponse, StreamingResponse
from openenv.core.env_server import create_app

from openra_env.models import OpenRAAction, OpenRAObservation
from openra_env.server.openra_environment import OpenRAEnvironment

app = create_app(
    OpenRAEnvironment,
    OpenRAAction,
    OpenRAObservation,
    env_name="openra_env",
)


# ── Try Agent: LLM demo endpoint ────────────────────────────────────────────

_TRY_MAX_TURNS = 30
_TRY_MAX_TIME = 300  # 5 minutes

_COMMENTARY_SYSTEM_PROMPT = (
    "You are a real-time commentator for an AI playing Command & Conquer: Red Alert. "
    "Given the AI's recent actions and current game state, write 1-2 sentences "
    "explaining what the AI is doing and why, in an engaging style. "
    "Keep it concise and accessible to viewers who may not know RTS games well."
)
_COMMENTARY_MAX_TOKENS = 150


def _sse(event_type: str, data: dict) -> str:
    """Format a Server-Sent Event."""
    return f"event: {event_type}\ndata: {json.dumps(data)}\n\n"


async def _generate_commentary(user_content: str, llm_config, broadcaster) -> None:
    """Generate commentary in the background and broadcast it."""
    import httpx as _httpx

    try:
        headers = dict(llm_config.extra_headers)
        if llm_config.api_key:
            headers["Authorization"] = f"Bearer {llm_config.api_key}"

        payload = {
            "model": llm_config.model,
            "messages": [
                {"role": "system", "content": _COMMENTARY_SYSTEM_PROMPT},
                {"role": "user", "content": user_content},
            ],
            "max_tokens": 400,
            "reasoning": {"effort": "low"},
            "temperature": 0.6,
            "top_p": 0.95,
        }

        async with _httpx.AsyncClient() as client:
            resp = await client.post(
                llm_config.base_url,
                headers=headers,
                json=payload,
                timeout=llm_config.request_timeout_s,
            )

        if resp.status_code != 200:
            return
        data = resp.json()
        msg = data["choices"][0]["message"]
        text = msg.get("content") or ""
        if not text:
            # Reasoning models may put output in 'reasoning' if content is empty
            text = msg.get("reasoning") or ""
            if text:
                sentences = [s.strip() for s in text.replace("\n", " ").split(".") if s.strip()]
                text = ". ".join(sentences[-2:]) + "." if sentences else ""
        if text:
            broadcaster._broadcast(_sse("commentary", {"text": text.strip()}))
    except Exception:
        pass  # Commentary is non-essential


class TryGameBroadcaster:
    """Manages a single game broadcast to multiple SSE subscribers."""

    def __init__(self):
        self._event_history: list[str] = []
        self._subscribers: set[asyncio.Queue] = set()
        self._game_running: bool = False
        self._game_task: asyncio.Task | None = None
        self._opponent: str = ""
        self._start_lock = asyncio.Lock()

    @property
    def game_running(self) -> bool:
        return self._game_running

    def subscribe(self) -> asyncio.Queue:
        queue: asyncio.Queue = asyncio.Queue()
        self._subscribers.add(queue)
        return queue

    def unsubscribe(self, queue: asyncio.Queue) -> None:
        self._subscribers.discard(queue)

    def _broadcast(self, event: str) -> None:
        self._event_history.append(event)
        for q in self._subscribers:
            q.put_nowait(event)

    async def replay_to(self, queue: asyncio.Queue) -> None:
        for event in list(self._event_history):
            await queue.put(event)

    async def start_game(self, opponent: str) -> None:
        async with self._start_lock:
            if self._game_running:
                return
            self._event_history.clear()
            self._opponent = opponent
            self._game_running = True
            self._game_task = asyncio.create_task(self._run_game(opponent))

    async def _run_game(self, opponent: str) -> None:
        try:
            async for event in _run_try_agent(opponent):
                self._broadcast(event)
        finally:
            self._game_running = False
            sentinel = _sse("_stream_end", {})
            for q in self._subscribers:
                q.put_nowait(sentinel)


_broadcaster = TryGameBroadcaster()


async def _run_try_agent(opponent: str):
    """Run LLM agent for one demo game, yielding SSE events."""
    from openra_env.agent import (
        SYSTEM_PROMPT,
        chat_completion,
        compose_pregame_briefing,
        compress_history,
        format_state_briefing,
        mcp_tools_to_openai,
    )
    from openra_env.config import LLMConfig
    from openra_env.mcp_ws_client import OpenRAMCPClient

    api_key = os.environ.get("OPENROUTER_API_KEY", "")
    if not api_key:
        yield _sse("error_event", {"message": "Server not configured for demo play (no API key)."})
        return

    llm_config = LLMConfig(
        api_key=api_key,
        model="stepfun/step-3.5-flash",
        base_url="https://openrouter.ai/api/v1/chat/completions",
        max_tokens=1500,
        temperature=1.0,
        top_p=0.95,
        reasoning_effort="low",
        extra_headers={
            "HTTP-Referer": "https://openra-rl.dev",
            "X-Title": "OpenRA-RL Try Agent",
        },
    )
    commentary_config = LLMConfig(
        api_key=api_key,
        model="stepfun/step-3.5-flash",
        base_url="https://openrouter.ai/api/v1/chat/completions",
        max_tokens=_COMMENTARY_MAX_TOKENS,
        request_timeout_s=15.0,
        extra_headers={
            "HTTP-Referer": "https://openra-rl.dev",
            "X-Title": "OpenRA-RL Commentary",
        },
    )

    # Configure opponent difficulty for the next game
    os.environ["BOT_TYPE"] = opponent.lower()

    yield _sse("status", {"message": f"Launching game vs {opponent} AI..."})

    try:
        async with OpenRAMCPClient(
            base_url="http://localhost:8000", message_timeout_s=300.0
        ) as env:
            yield _sse("status", {"message": "Resetting environment..."})
            await env.reset()

            # Discover tools
            mcp_tools = await env.list_tools()
            openai_tools = mcp_tools_to_openai(mcp_tools)

            # Start + end planning to trigger session start (unpauses game)
            yield _sse("status", {"message": "Starting game session..."})
            await env.call_tool("start_planning_phase")
            await env.call_tool("end_planning_phase", strategy="Demo game - aggressive rush")
            yield _sse("status", {"message": f"Game started. {len(mcp_tools)} tools available."})

            # Initialize conversation
            messages = [{"role": "system", "content": SYSTEM_PROMPT}]

            # Get initial state and compose briefing
            state = await env.call_tool("get_game_state")
            briefing = compose_pregame_briefing(state)

            messages.append({
                "role": "user",
                "content": (
                    f"Game started!\n\n{briefing}\n\n"
                    f"## Current State\n```json\n{json.dumps(state, indent=2)}\n```\n\n"
                    f"ACT NOW! Deploy your MCV immediately, then start building power plant + barracks. "
                    f"Expand fast — every idle second costs you. Use plan() to chain: "
                    f"deploy MCV → build power plant → build barracks → build refinery. "
                    f"Then focus on economy (3+ refineries) and defense turrets toward the enemy."
                ),
            })

            yield _sse("game_state", {
                "tick": state.get("tick", 0),
                "units": state.get("own_units", 0),
                "buildings": state.get("own_buildings", 0),
                "cash": state.get("economy", {}).get("cash", 0),
            })

            total_tool_calls = 0
            total_api_calls = 0
            start_time = time.time()
            game_done = False
            consecutive_errors = 0

            for turn in range(1, _TRY_MAX_TURNS + 1):
                elapsed = time.time() - start_time
                if elapsed >= _TRY_MAX_TIME:
                    yield _sse("status", {"message": f"Time limit reached ({_TRY_MAX_TIME}s)."})
                    break

                # Compress history to stay within context limits
                messages = compress_history(messages, keep_last=40)

                # Inject state briefing (skip first turn — initial state already sent)
                if total_api_calls > 0:
                    try:
                        briefing_state = await env.call_tool("get_game_state")
                        brief = format_state_briefing(briefing_state)
                        if brief:
                            messages.append({"role": "user", "content": brief})
                        if isinstance(briefing_state, dict) and briefing_state.get("done"):
                            game_done = True
                            yield _sse("done", {
                                "result": briefing_state.get("result", "?"),
                                "tick": briefing_state.get("tick", 0),
                            })
                            break
                    except Exception:
                        pass

                # Call LLM
                try:
                    response = await chat_completion(messages, openai_tools, llm_config)
                except Exception as e:
                    yield _sse("error_event", {"message": f"LLM error: {e}"})
                    break

                total_api_calls += 1
                choice = response["choices"][0]
                assistant_msg = choice["message"]
                messages.append(assistant_msg)

                # Emit LLM reasoning
                if assistant_msg.get("content"):
                    yield _sse("llm", {"content": assistant_msg["content"][:500]})

                yield _sse("turn", {
                    "turn": turn,
                    "api_calls": total_api_calls,
                    "elapsed": round(elapsed),
                })

                # Handle tool calls
                tool_calls = assistant_msg.get("tool_calls", [])
                if not tool_calls:
                    messages.append({
                        "role": "user",
                        "content": "Please use the game tools to take action.",
                    })
                    continue

                for tc in tool_calls:
                    fn_name = tc["function"]["name"]
                    try:
                        fn_args = json.loads(tc["function"].get("arguments", "{}"))
                    except (json.JSONDecodeError, TypeError):
                        fn_args = {}

                    total_tool_calls += 1

                    args_str = json.dumps(fn_args)
                    if len(args_str) > 120:
                        args_str = args_str[:120] + "..."
                    yield _sse("tool_call", {"name": fn_name, "args": args_str})

                    try:
                        result = await env.call_tool(fn_name, **fn_args)
                        consecutive_errors = 0
                    except Exception as e:
                        result = {"error": str(e)}

                    # Detect game crash
                    if isinstance(result, dict) and "connection lost" in str(
                        result.get("error", "")
                    ).lower():
                        consecutive_errors += 1
                        if consecutive_errors >= 3:
                            yield _sse("error_event", {"message": "Game connection lost."})
                            game_done = True

                    result_str = (
                        json.dumps(result) if not isinstance(result, str) else result
                    )
                    messages.append({
                        "role": "tool",
                        "tool_call_id": tc["id"],
                        "content": result_str,
                    })

                    # Check game over
                    if isinstance(result, dict):
                        if result.get("done"):
                            game_done = True
                            yield _sse("done", {
                                "result": result.get("result", "?"),
                                "tick": result.get("tick", 0),
                            })
                        elif "tick" in result and "economy" in result:
                            yield _sse("game_state", {
                                "tick": result.get("tick", 0),
                                "units": result.get("own_units", 0),
                                "buildings": result.get("own_buildings", 0),
                                "cash": result.get("economy", {}).get("cash", 0),
                            })

                # Fire-and-forget async commentary (doesn't block game loop)
                if tool_calls and not game_done:
                    action_summaries = []
                    for tc in tool_calls:
                        fn = tc["function"]["name"]
                        try:
                            fa = json.loads(tc["function"].get("arguments", "{}"))
                        except (json.JSONDecodeError, TypeError):
                            fa = {}
                        action_summaries.append(f"{fn}({json.dumps(fa)})")

                    commentary_user = (
                        f"Turn {turn} actions:\n"
                        + "\n".join(f"- {a}" for a in action_summaries[:8])
                    )
                    asyncio.create_task(_generate_commentary(
                        commentary_user, commentary_config, _broadcaster,
                    ))

                if game_done:
                    break

                if choice.get("finish_reason") == "stop" and not tool_calls:
                    messages.append({
                        "role": "user",
                        "content": "Continue playing. Use game tools to check state and take actions.",
                    })

            # Surrender if game didn't end naturally
            if not game_done:
                try:
                    await env.call_tool("surrender")
                except Exception:
                    pass

            # Emit final scorecard
            try:
                final = await env.call_tool("get_game_state")
                mil = final.get("military", {})
                eco = final.get("economy", {})
                yield _sse("final", {
                    "result": final.get("result", "ongoing"),
                    "tick": final.get("tick", 0),
                    "turns": total_api_calls,
                    "tool_calls": total_tool_calls,
                    "elapsed": round(time.time() - start_time),
                    "kills_cost": mil.get("kills_cost", 0),
                    "deaths_cost": mil.get("deaths_cost", 0),
                    "units_killed": mil.get("units_killed", 0),
                    "units_lost": mil.get("units_lost", 0),
                    "cash": eco.get("cash", 0),
                    "units": final.get("own_units", 0),
                    "buildings": final.get("own_buildings", 0),
                })
            except Exception:
                pass

    except Exception as e:
        yield _sse("error_event", {"message": str(e)})


@app.get("/try-agent")
async def try_agent(
    opponent: str = Query("Normal", pattern="^(Easy|Normal|Hard)$"),
):
    """SSE stream of an LLM agent playing Red Alert.

    Multiple viewers can watch simultaneously. The first request starts
    a new game; subsequent requests join as spectators of the ongoing game.
    """
    queue = _broadcaster.subscribe()

    if _broadcaster.game_running:
        await queue.put(_sse("status", {"message": "Joining ongoing game as spectator..."}))
        await _broadcaster.replay_to(queue)
    else:
        await _broadcaster.start_game(opponent)

    async def stream():
        try:
            while True:
                event = await asyncio.wait_for(queue.get(), timeout=360)
                if '"_stream_end"' in event:
                    break
                yield event
        except asyncio.TimeoutError:
            pass
        finally:
            _broadcaster.unsubscribe(queue)

    return StreamingResponse(
        stream(),
        media_type="text/event-stream",
        headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
    )


LANDING_PAGE = """\
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenRA-RL &mdash; OpenEnv Environment</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Teko:wght@400;600;700&display=swap" rel="stylesheet">
<style>
/* === Reset & base === */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
  font-family: 'Share Tech Mono', monospace;
  background: radial-gradient(circle at center, #1a0505 0%, #050505 100%);
  color: #d1d5db;
  min-height: 100vh;
}
a { color: #d1d5db; text-decoration: none; transition: color .2s; }
a:hover { color: #fff; }
h1, h2, h3, h4, .font-teko {
  font-family: 'Teko', sans-serif;
  letter-spacing: 1px;
  text-transform: uppercase;
}

/* === Scanlines === */
.scanlines {
  background: linear-gradient(
    to bottom,
    rgba(255,255,255,0),
    rgba(255,255,255,0) 50%,
    rgba(0,0,0,0.2) 50%,
    rgba(0,0,0,0.2)
  );
  background-size: 100% 4px;
  position: fixed; inset: 0;
  pointer-events: none; z-index: 50;
}

/* === CRT flicker === */
.crt-flicker { animation: crt .15s infinite; }
@keyframes crt {
  0% { opacity: .98; } 50% { opacity: 1; } 100% { opacity: .99; }
}

/* === Text effects === */
.terminal-text { color: #84cc16; text-shadow: 0 0 5px rgba(132,204,22,.5); }
.alert-text    { color: #ef4444; text-shadow: 0 0 8px rgba(239,68,68,.8); }

/* === Buttons === */
.btn-soviet {
  display: inline-flex; align-items: center; gap: .5rem;
  background: #dc2626; border: 2px solid #f87171;
  box-shadow: 4px 4px 0 #000; transition: all .1s;
  color: #fff; font-family: 'Teko', sans-serif;
  font-size: 1.6rem; padding: .4rem 1.8rem;
  text-transform: uppercase; cursor: pointer;
}
.btn-soviet:hover { transform: translate(2px,2px); box-shadow: 2px 2px 0 #000; background: #ef4444; color: #fff; }

.btn-ghost {
  display: inline-flex; align-items: center; gap: .5rem;
  background: #171717; border: 2px solid #525252;
  box-shadow: 4px 4px 0 #000; transition: all .1s;
  color: #a3a3a3; font-family: 'Teko', sans-serif;
  font-size: 1.6rem; padding: .4rem 1.8rem;
  text-transform: uppercase; cursor: pointer;
}
.btn-ghost:hover { transform: translate(2px,2px); box-shadow: 2px 2px 0 #000; border-color: #737373; color: #fff; }

/* === Military card === */
.card-military {
  background: #121212; border: 2px solid #262626;
  border-left: 4px solid #dc2626;
  box-shadow: 6px 6px 0 rgba(0,0,0,.8);
  transition: all .2s; padding: 1.5rem;
}
.card-military:hover { border-color: #dc2626; transform: translateY(-4px); box-shadow: 6px 10px 0 rgba(0,0,0,.8); }
.card-military h3 { color: #fff; font-size: 1.6rem; margin-bottom: .3rem; }
.card-military p { color: #9ca3af; font-size: .85rem; margin-bottom: .8rem; }
.card-military a { color: #ef4444; font-size: .85rem; }
.card-military a:hover { color: #f87171; }

/* === Nav === */
nav {
  border-bottom: 2px solid #991b1b;
  background: rgba(0,0,0,.9);
  position: sticky; top: 0; z-index: 40;
  backdrop-filter: blur(4px);
}
.nav-inner {
  max-width: 72rem; margin: 0 auto;
  padding: 0 1.5rem;
  display: flex; align-items: center; justify-content: space-between;
  height: 4rem;
}
.nav-logo { display: flex; align-items: center; gap: .6rem; }
.nav-logo svg { width: 2rem; height: 2rem; color: #dc2626; animation: spin 4s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.nav-logo span { font-family: 'Teko', sans-serif; font-size: 1.8rem; font-weight: 700; color: #fff; letter-spacing: .15em; }
.nav-logo .rl { color: #dc2626; }
.nav-links { display: flex; gap: 1.5rem; align-items: center; }
.nav-links a {
  font-family: 'Teko', sans-serif; font-size: 1.15rem;
  letter-spacing: .1em; color: #9ca3af;
  text-transform: uppercase;
}
.nav-links a:hover { color: #fff; }

/* === Hero === */
.hero {
  border-bottom: 4px solid #991b1b;
  padding: 5rem 1rem 6rem;
  text-align: center;
  position: relative;
}
.hero::before {
  content: ''; position: absolute; inset: 0; opacity: .5;
  mix-blend-mode: overlay; pointer-events: none;
  background-image: url('https://www.transparenttextures.com/patterns/carbon-fibre.png');
}
.hero > * { position: relative; z-index: 1; }
.hero .subtitle {
  font-size: 1.1rem; margin-bottom: 1rem;
  font-weight: 700; display: flex; align-items: center; justify-content: center; gap: .5rem;
}
.hero h1 { font-size: clamp(3rem, 10vw, 5.5rem); font-weight: 700; line-height: 1; margin-bottom: 1.2rem; }
.hero .desc {
  font-size: 1.1rem; max-width: 40rem; margin: 0 auto 2.5rem;
  line-height: 1.7; background: rgba(0,0,0,.5);
  padding: 1.2rem 1.5rem; border: 1px solid #262626; border-radius: 4px;
}
.hero .desc strong { color: #fff; display: block; margin-top: .5rem; }
.hero .buttons { display: flex; flex-wrap: wrap; gap: 1.2rem; justify-content: center; }

/* === Sections === */
.section { padding: 4rem 1rem; max-width: 72rem; margin: 0 auto; }
.section-dark { background: #050505; border-top: 1px solid #262626; border-bottom: 1px solid #262626; }
.section h2 { color: #fff; font-size: 2.4rem; margin-bottom: 1.5rem; }
.cards-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1.5rem; }

/* === Terminal === */
.terminal { border: 1px solid #404040; border-radius: 8px; overflow: hidden; box-shadow: 0 0 30px rgba(0,0,0,1); }
.terminal-bar {
  background: #1a1a1a; padding: .5rem 1rem; border-bottom: 1px solid #404040;
  display: flex; align-items: center; gap: .5rem;
}
.terminal-dot { width: .7rem; height: .7rem; border-radius: 50%; }
.terminal-bar span { margin-left: .5rem; font-size: .75rem; color: #9ca3af; }
.terminal-body { background: #0d0d0d; padding: 1.5rem; overflow-x: auto; font-size: .85rem; line-height: 1.8; }
.terminal-body pre { margin: 0; white-space: pre; }
.t-prompt { color: #6b7280; }
.t-cmd { color: #4ade80; }
.t-kw { color: #c084fc; }
.t-str { color: #facc15; }
.t-fn { color: #38bdf8; }
.t-comment { color: #6b7280; }
.t-plain { color: #d1d5db; }

/* === Footer === */
footer {
  background: #000; border-top: 2px solid #7f1d1d;
  margin-top: 4rem; padding: 3rem 1.5rem;
}
.footer-inner { max-width: 72rem; margin: 0 auto; display: flex; flex-wrap: wrap; gap: 2rem; justify-content: space-between; }
.footer-brand { max-width: 24rem; }
.footer-brand .logo { font-family: 'Teko', sans-serif; font-size: 2rem; font-weight: 700; color: #fff; letter-spacing: .15em; display: flex; align-items: center; gap: .5rem; }
.footer-brand .logo .rl { color: #dc2626; }
.footer-status { margin-top: 1rem; font-size: .8rem; color: #84cc16; text-shadow: 0 0 5px rgba(132,204,22,.5); line-height: 1.8; }
.footer-col h3 { font-family: 'Teko', sans-serif; font-size: 1.4rem; color: #fff; margin-bottom: .8rem; }
.footer-col ul { list-style: none; }
.footer-col li { margin-bottom: .5rem; }
.footer-col a { font-size: .85rem; display: flex; align-items: center; gap: .4rem; }
.footer-col a:hover { color: #ef4444; }
.chevron::before { content: '\\203A'; margin-right: .3rem; }

/* === Fade in === */
.fade-in { animation: fadeIn .7s ease-in-out; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }

/* === Responsive === */
@media (max-width: 640px) {
  .nav-links { display: none; }
  .cards-grid { grid-template-columns: 1fr; }
  .terminal-section { grid-template-columns: 1fr; }
}
.terminal-section {
  display: grid; grid-template-columns: 1fr 1fr; gap: 3rem; align-items: center;
}
@media (max-width: 900px) { .terminal-section { grid-template-columns: 1fr; } }
</style>
</head>
<body>
<div class="scanlines"></div>

<!-- Nav -->
<nav>
  <div class="nav-inner">
    <a href="/" class="nav-logo">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="miter">
        <circle cx="12" cy="12" r="10"/>
        <circle cx="12" cy="12" r="6" stroke-opacity="0.5"/>
        <circle cx="12" cy="12" r="2" fill="currentColor"/>
        <path d="M12 12l8.5-8.5"/>
        <path d="M12 2v10H2" stroke-opacity="0.5" stroke-dasharray="2 2"/>
      </svg>
      <span>OPENRA<span class="rl">-RL</span></span>
    </a>
    <div class="nav-links">
      <a href="/try" style="color:#ef4444;font-weight:700;">TRY</a>
      <a href="https://openra-rl.dev/docs/getting-started">DOCS</a>
      <a href="/docs">API</a>
      <a href="https://github.com/yxc20089/OpenRA-RL">GITHUB</a>
    </div>
  </div>
</nav>

<!-- Hero -->
<section class="hero crt-flicker fade-in">
  <div class="subtitle terminal-text">
    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 17l6-6-6-6"/><path d="M12 19h8"/></svg>
    SYSTEM OVERRIDE ACTIVE
  </div>
  <h1 class="alert-text">OPENRA-RL</h1>
  <div class="desc">
    OpenEnv environment for training AI agents to play
    <strong>Red Alert</strong> through the OpenRA engine.
    Connect via WebSocket or HTTP, send actions, observe the battlefield.
  </div>
  <div class="buttons">
    <a href="/try" class="btn-soviet">
      WATCH AI PLAY
      <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
    </a>
    <a href="https://openra-rl.dev/docs/getting-started" class="btn-ghost">
      DOCUMENTATION
      <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>
    </a>
    <a href="https://openra-rl-openra-bench.hf.space" class="btn-ghost">
      LEADERBOARD
      <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6-6 6 6"/><path d="M12 3v18"/><path d="M3 21h18"/></svg>
    </a>
  </div>
</section>

<!-- Endpoint cards -->
<div class="section">
  <h2>Endpoints</h2>
  <div class="cards-grid">
    <div class="card-military">
      <h3>API DOCS</h3>
      <p>Interactive Swagger UI with all REST and WebSocket endpoints.</p>
      <a href="/docs">/docs &rarr;</a>
    </div>
    <div class="card-military">
      <h3>HEALTH CHECK</h3>
      <p>Server status and readiness probe for monitoring.</p>
      <a href="/health">/health &rarr;</a>
    </div>
    <div class="card-military">
      <h3>ENV SCHEMA</h3>
      <p>JSON schemas for actions, observations, and game state.</p>
      <a href="/schema">/schema &rarr;</a>
    </div>
  </div>
</div>

<!-- Terminal / code section -->
<div class="section-dark">
  <div class="section">
    <div class="terminal-section">
      <div>
        <h2>Connect to Environment</h2>
        <p style="color:#9ca3af;line-height:1.7;margin-bottom:1.5rem;">
          Use the Python client to connect, reset the environment,
          and step through the game loop. Works with both local
          Docker and this HuggingFace-hosted server.
        </p>
        <a href="https://openra-rl.dev/docs/api-reference" class="btn-soviet" style="font-size:1.3rem;">
          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19V5a2 2 0 012-2h13l3 3v13a2 2 0 01-2 2H6a2 2 0 01-2-2z"/><path d="M9 3v4h4"/></svg>
          API REFERENCE
        </a>
      </div>
      <div class="terminal">
        <div class="terminal-bar">
          <div class="terminal-dot" style="background:#ef4444;"></div>
          <div class="terminal-dot" style="background:#eab308;"></div>
          <div class="terminal-dot" style="background:#22c55e;"></div>
          <span>terminal</span>
        </div>
        <div class="terminal-body">
<pre><span class="t-prompt">$ </span><span class="t-cmd">pip install</span><span class="t-plain"> openra-rl</span>

<span class="t-kw">from</span><span class="t-plain"> openra_env.client </span><span class="t-kw">import</span><span class="t-plain"> OpenRAEnv</span>
<span class="t-kw">from</span><span class="t-plain"> openra_env.models </span><span class="t-kw">import</span><span class="t-plain"> OpenRAAction</span>

<span class="t-plain">url = </span><span class="t-str">"https://openra-rl-openra-rl.hf.space"</span>

<span class="t-kw">async with</span><span class="t-plain"> </span><span class="t-fn">OpenRAEnv</span><span class="t-plain">(url) </span><span class="t-kw">as</span><span class="t-plain"> env:</span>
<span class="t-plain">    obs = </span><span class="t-kw">await</span><span class="t-plain"> env.</span><span class="t-fn">reset</span><span class="t-plain">()</span>
<span class="t-plain">    </span><span class="t-kw">while not</span><span class="t-plain"> obs.done:</span>
<span class="t-plain">        action = your_agent(obs)</span>
<span class="t-plain">        obs = </span><span class="t-kw">await</span><span class="t-plain"> env.</span><span class="t-fn">step</span><span class="t-plain">(action)</span></pre>
        </div>
      </div>
    </div>
  </div>
</div>

<!-- Footer -->
<footer>
  <div class="footer-inner">
    <div class="footer-brand">
      <div class="logo">
        <svg viewBox="0 0 24 24" fill="none" stroke="#dc2626" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="miter" width="28" height="28">
          <circle cx="12" cy="12" r="10"/>
          <circle cx="12" cy="12" r="6" stroke-opacity="0.5"/>
          <circle cx="12" cy="12" r="2" fill="#dc2626"/>
          <path d="M12 12l8.5-8.5"/>
        </svg>
        OPENRA<span class="rl">-RL</span>
      </div>
      <div class="footer-status">
        &gt; SYSTEM STATUS: OPERATIONAL<br>
        &gt; MISSION: TRAIN AI TO CONQUER<br>
        &gt; &copy; 2025 OPENRA-RL CONTRIBUTORS.
      </div>
    </div>
    <div class="footer-col">
      <h3>Intel</h3>
      <ul>
        <li><a href="https://openra-rl.dev/docs/getting-started"><span class="chevron"></span>Getting Started</a></li>
        <li><a href="https://openra-rl.dev/docs/architecture"><span class="chevron"></span>Architecture</a></li>
        <li><a href="https://openra-rl.dev/docs/api-reference"><span class="chevron"></span>API Reference</a></li>
      </ul>
    </div>
    <div class="footer-col">
      <h3>Alliances</h3>
      <ul>
        <li><a href="https://www.openra.net/"><span class="chevron"></span>OpenRA Engine</a></li>
        <li><a href="https://huggingface.co/openra-rl"><span class="chevron"></span>HuggingFace</a></li>
        <li><a href="https://openra-rl-openra-bench.hf.space"><span class="chevron"></span>Leaderboard</a></li>
      </ul>
    </div>
  </div>
</footer>
</body>
</html>"""


@app.get("/", response_class=HTMLResponse)
async def root():
    """Landing page for the HuggingFace Space."""
    return LANDING_PAGE


# ── Try Page: Watch AI Play ──────────────────────────────────────────────────

TRY_PAGE = """\
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Try &mdash; Watch AI Play Red Alert</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Share+Tech+Mono&family=Teko:wght@400;600;700&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
  font-family: 'Share Tech Mono', monospace;
  background: radial-gradient(circle at center, #1a0505 0%, #050505 100%);
  color: #d1d5db;
  min-height: 100vh;
}
a { color: #d1d5db; text-decoration: none; transition: color .2s; }
a:hover { color: #fff; }
h1, h2, h3, .font-teko {
  font-family: 'Teko', sans-serif;
  letter-spacing: 1px;
  text-transform: uppercase;
}
.scanlines {
  background: linear-gradient(to bottom, rgba(255,255,255,0), rgba(255,255,255,0) 50%, rgba(0,0,0,0.2) 50%, rgba(0,0,0,0.2));
  background-size: 100% 4px;
  position: fixed; inset: 0; pointer-events: none; z-index: 50;
}
.terminal-text { color: #84cc16; text-shadow: 0 0 5px rgba(132,204,22,.5); }
.alert-text { color: #ef4444; text-shadow: 0 0 8px rgba(239,68,68,.8); }

nav {
  border-bottom: 2px solid #991b1b;
  background: rgba(0,0,0,.9);
  position: sticky; top: 0; z-index: 40;
  backdrop-filter: blur(4px);
}
.nav-inner {
  max-width: 72rem; margin: 0 auto; padding: 0 1.5rem;
  display: flex; align-items: center; justify-content: space-between; height: 4rem;
}
.nav-logo { display: flex; align-items: center; gap: .6rem; }
.nav-logo svg { width: 2rem; height: 2rem; color: #dc2626; animation: spin 4s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.nav-logo span { font-family: 'Teko', sans-serif; font-size: 1.8rem; font-weight: 700; color: #fff; letter-spacing: .15em; }
.nav-logo .rl { color: #dc2626; }
.nav-links { display: flex; gap: 1.5rem; align-items: center; }
.nav-links a { font-family: 'Teko', sans-serif; font-size: 1.15rem; letter-spacing: .1em; color: #9ca3af; text-transform: uppercase; }
.nav-links a:hover { color: #fff; }

.container { max-width: 56rem; margin: 0 auto; padding: 2rem 1.5rem; }
.header { text-align: center; margin-bottom: 2rem; }
.header h1 { font-size: 2.5rem; color: #fff; margin-bottom: .5rem; }
.header p { color: #9ca3af; font-size: .95rem; }

.controls {
  display: flex; gap: 1rem; align-items: center; justify-content: center;
  margin-bottom: 1.5rem; flex-wrap: wrap;
}
.controls select {
  font-family: 'Share Tech Mono', monospace;
  background: #121212; color: #d1d5db; border: 2px solid #525252;
  padding: .5rem 1rem; font-size: 1rem; cursor: pointer;
}
.controls select:hover { border-color: #737373; }
.btn-soviet {
  display: inline-flex; align-items: center; gap: .5rem;
  background: #dc2626; border: 2px solid #f87171;
  box-shadow: 4px 4px 0 #000; transition: all .1s;
  color: #fff; font-family: 'Teko', sans-serif;
  font-size: 1.6rem; padding: .4rem 1.8rem;
  text-transform: uppercase; cursor: pointer;
}
.btn-soviet:hover { transform: translate(2px,2px); box-shadow: 2px 2px 0 #000; background: #ef4444; }
.btn-soviet:disabled { opacity: .5; cursor: not-allowed; transform: none; box-shadow: 4px 4px 0 #000; }

.game-log {
  background: #0a0a0a; border: 2px solid #262626; border-left: 4px solid #dc2626;
  padding: 1rem 1.2rem; font-size: .82rem; line-height: 1.7;
  height: 420px; overflow-y: auto; white-space: pre-wrap; word-break: break-word;
  margin-bottom: 1.5rem;
}
.game-log .log-status { color: #84cc16; }
.game-log .log-turn { color: #facc15; }
.game-log .log-llm { color: #c084fc; }
.game-log .log-tool { color: #38bdf8; }
.game-log .log-state { color: #6b7280; }
.game-log .log-done { color: #ef4444; font-weight: bold; }
.game-log .log-error { color: #ef4444; }
.game-log .log-commentary { color: #f59e0b; font-style: italic; padding-left: 1em; border-left: 2px solid #f59e0b; margin: 2px 0; }

.scorecard {
  background: #121212; border: 2px solid #262626; padding: 1.5rem;
  display: none;
}
.scorecard h2 { color: #fff; font-size: 1.8rem; margin-bottom: 1rem; }
.scorecard table { width: 100%; border-collapse: collapse; }
.scorecard td { padding: .4rem .8rem; border-bottom: 1px solid #1a1a1a; font-size: .85rem; }
.scorecard td:first-child { color: #9ca3af; }
.scorecard td:last-child { color: #fff; text-align: right; }
.scorecard .result-win { color: #22c55e; font-size: 1.2rem; font-weight: bold; }
.scorecard .result-loss { color: #ef4444; font-size: 1.2rem; font-weight: bold; }

footer {
  background: #000; border-top: 2px solid #7f1d1d;
  margin-top: 3rem; padding: 1.5rem;
  text-align: center; font-size: .8rem; color: #6b7280;
}
</style>
</head>
<body>
<div class="scanlines"></div>

<nav>
  <div class="nav-inner">
    <a href="/" class="nav-logo">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" stroke-linejoin="miter">
        <circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6" stroke-opacity="0.5"/>
        <circle cx="12" cy="12" r="2" fill="currentColor"/><path d="M12 12l8.5-8.5"/>
        <path d="M12 2v10H2" stroke-opacity="0.5" stroke-dasharray="2 2"/>
      </svg>
      <span>OPENRA<span class="rl">-RL</span></span>
    </a>
    <div class="nav-links">
      <a href="/try" style="color:#ef4444;font-weight:700;">TRY</a>
      <a href="https://openra-rl.dev/docs/getting-started">DOCS</a>
      <a href="/docs">API</a>
      <a href="https://github.com/yxc20089/OpenRA-RL">GITHUB</a>
    </div>
  </div>
</nav>

<div class="container">
  <div class="header">
    <h1 class="alert-text">Watch AI Play</h1>
    <p>A pre-configured LLM agent plays Red Alert against the built-in AI. No setup needed.</p>
  </div>

  <div class="controls">
    <select id="opponent">
      <option value="Easy">Easy</option>
      <option value="Normal" selected>Normal</option>
      <option value="Hard">Hard</option>
    </select>
    <button id="playBtn" class="btn-soviet" onclick="startGame()">
      <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
      WATCH AI PLAY
    </button>
  </div>

  <div id="gameLog" class="game-log">Waiting to start...\n</div>

  <div id="scorecard" class="scorecard">
    <h2>Scorecard</h2>
    <table id="scorecardTable"></table>
  </div>
</div>

<footer>&copy; 2025 OpenRA-RL Contributors &mdash; <a href="/">Home</a></footer>

<script>
let eventSource = null;

function log(msg, cls) {
  const el = document.getElementById('gameLog');
  const span = document.createElement('span');
  span.className = cls || '';
  span.textContent = msg + '\\n';
  el.appendChild(span);
  el.scrollTop = el.scrollHeight;
}

function startGame() {
  const btn = document.getElementById('playBtn');
  const logEl = document.getElementById('gameLog');
  const scorecard = document.getElementById('scorecard');
  const opponent = document.getElementById('opponent').value;

  // Reset UI
  logEl.innerHTML = '';
  scorecard.style.display = 'none';
  btn.disabled = true;
  document.getElementById('opponent').disabled = true;
  btn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg> WATCHING...';

  log('Connecting to game server...', 'log-status');

  fetch('/try-agent?opponent=' + encodeURIComponent(opponent))
    .then(response => {
      if (!response.ok) {
        log('Server error: ' + response.status, 'log-error');
        resetBtn();
        return;
      }

      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let buffer = '';
      let currentEvent = '';

      function processChunk() {
        reader.read().then(({done, value}) => {
          if (done) {
            resetBtn();
            return;
          }

          buffer += decoder.decode(value, {stream: true});
          const lines = buffer.split('\\n');
          buffer = lines.pop();

          for (const line of lines) {
            if (line.startsWith('event: ')) {
              currentEvent = line.slice(7).trim();
            } else if (line.startsWith('data: ')) {
              try {
                const data = JSON.parse(line.slice(6));
                handleEvent(currentEvent, data);
              } catch(e) {}
            }
          }

          processChunk();
        }).catch(err => {
          log('Connection lost: ' + err.message, 'log-error');
          resetBtn();
        });
      }

      processChunk();
    })
    .catch(err => {
      log('Failed to connect: ' + err.message, 'log-error');
      resetBtn();
    });
}

function handleEvent(type, data) {
  switch(type) {
    case 'status':
      log(data.message, 'log-status');
      break;
    case 'turn':
      log('[Turn ' + data.turn + '] API calls: ' + data.api_calls + ' | ' + data.elapsed + 's', 'log-turn');
      break;
    case 'llm':
      if (data.content) {
        const text = data.content.length > 300 ? data.content.slice(0, 300) + '...' : data.content;
        log('  AI: ' + text, 'log-llm');
      }
      break;
    case 'tool_call':
      log('  >> ' + data.name + '(' + (data.args || '') + ')', 'log-tool');
      break;
    case 'game_state':
      log('  tick=' + data.tick + ' units=' + data.units + ' buildings=' + data.buildings + ' $' + data.cash, 'log-state');
      break;
    case 'done':
      log('\\nGAME OVER: ' + (data.result || '?').toUpperCase() + ' (tick ' + data.tick + ')', 'log-done');
      break;
    case 'final':
      showScorecard(data);
      resetBtn();
      break;
    case 'commentary':
      if (data.text) {
        log('  [COMMENTARY] ' + data.text, 'log-commentary');
      }
      break;
    case 'error_event':
      log('Error: ' + (data.message || 'Unknown'), 'log-error');
      break;
  }
}

function showScorecard(data) {
  const sc = document.getElementById('scorecard');
  const tbl = document.getElementById('scorecardTable');
  const result = (data.result || 'ongoing').toUpperCase();
  const cls = result === 'WIN' ? 'result-win' : 'result-loss';

  tbl.innerHTML =
    '<tr><td>Result</td><td class="' + cls + '">' + result + '</td></tr>' +
    '<tr><td>Game Ticks</td><td>' + data.tick + '</td></tr>' +
    '<tr><td>LLM Turns</td><td>' + data.turns + '</td></tr>' +
    '<tr><td>Tool Calls</td><td>' + data.tool_calls + '</td></tr>' +
    '<tr><td>Duration</td><td>' + data.elapsed + 's</td></tr>' +
    '<tr><td>Units Killed</td><td>' + data.units_killed + '</td></tr>' +
    '<tr><td>Units Lost</td><td>' + data.units_lost + '</td></tr>' +
    '<tr><td>Kill Value</td><td>$' + data.kills_cost + '</td></tr>' +
    '<tr><td>Death Value</td><td>$' + data.deaths_cost + '</td></tr>' +
    '<tr><td>Cash Remaining</td><td>$' + data.cash + '</td></tr>' +
    '<tr><td>Own Units</td><td>' + data.units + '</td></tr>' +
    '<tr><td>Own Buildings</td><td>' + data.buildings + '</td></tr>';

  sc.style.display = 'block';
}

function resetBtn() {
  const btn = document.getElementById('playBtn');
  btn.disabled = false;
  btn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg> WATCH AI PLAY';
  document.getElementById('opponent').disabled = false;
}

// Auto-connect if a game is currently running
fetch('/try-status')
  .then(r => r.json())
  .then(status => {
    if (status.game_running) {
      if (status.opponent) {
        document.getElementById('opponent').value = status.opponent;
      }
      startGame();
    }
  })
  .catch(() => {});
</script>
</body>
</html>"""


@app.get("/try-status")
async def try_status():
    """Check if a game is currently running."""
    return {
        "game_running": _broadcaster.game_running,
        "opponent": _broadcaster._opponent if _broadcaster.game_running else "",
    }


@app.get("/try", response_class=HTMLResponse)
async def try_page():
    """Interactive page to watch an LLM agent play Red Alert."""
    return TRY_PAGE


def main():
    import uvicorn

    uvicorn.run(
        app,
        host="0.0.0.0",
        port=8000,
        ws_ping_interval=None,
        ws_ping_timeout=None,
    )


if __name__ == "__main__":
    main()
