"""多世界平台服务:落地页选世界 + 每世界独立邀请码(隔离)+ cookie 会话 + 离开暂停续播。

运行(部署):
  ~/miniconda3/envs/genesis-world/bin/python -m genesis.server.live_server
为某个世界生成邀请码(一码一会话,绑定该世界):
  ~/miniconda3/envs/genesis-world/bin/python -m genesis.server.live_server gen soldierisland 20

鉴权模型:静态资源公开;凡触发引擎的 /api/* 都要 cookie 里的有效会话,没有就回 401 {need_invite:true}。
邀请码与世界一一绑定:激活某码 → 会话绑到该码的世界 → 该会话之后所有 /api/* 都路由到那个世界的引擎池。
世界之间完全隔离:各自的邀请码、各自的会话、各自的引擎池与存档。
"""

from __future__ import annotations

import json
import os
from http.cookies import SimpleCookie
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import parse_qs, urlparse

from genesis.manifest.builder import WorldBuilder
from genesis.obs.logging_setup import get_logger
from genesis.server.engine_pool import EngineManager
from genesis.server.store import SessionStore
from genesis.world.pack import discover_worlds, validate_world

logger = get_logger("server.live_server")
ROOT = Path(__file__).resolve().parents[3]
LANDING = "/src/genesis/viz/web/platform.html"
GAME = "/src/genesis/viz/web/mystery.html"

_platform: "Platform | None" = None
_store: SessionStore | None = None


def _stage(builder: WorldBuilder, viz: dict | None) -> dict:
    characters = [{
        "name": cm.name, "age": cm.age, "mbti": cm.mbti, "occupation": cm.occupation,
        "backstory": getattr(cm, "backstory", ""),
        "sprite_dir": cm.art.sprite_path if cm.art else None, "life_goal": "", "sub_goals": [],
    } for cm in builder.characters.values()]
    places = [{"id": pid, "name": pm.name, "geography": pm.geography, "objects": [],
               "adjacent": [builder.places[a].name for a in pm.adjacent if a in builder.places]}
              for pid, pm in builder.places.items()]
    killer = next((cm.name for cm in builder.characters.values()
                   if "凶手" in (getattr(cm, "role", "") or "")), None)
    return {"places": places, "characters": characters, "killer": killer, "viz": viz}


class Platform:
    """多世界平台:发现所有合规世界,各自一套 viz/stage/引擎池,共享一个邀请码/会话 store。"""

    def __init__(self, store: SessionStore, shared_llm, *, brain: str, rate: float,
                 max_engines: int, idle_timeout: float) -> None:
        self.store = store
        self.worlds: dict[str, dict] = {}        # id → {viz, stage, mgr, meta}
        for wid in discover_worlds(str(ROOT / "worlds")):
            wdir = ROOT / "worlds" / wid
            if any(i.startswith("[E]") for i in validate_world(wdir)):
                logger.warning("世界 %s 校验未过,跳过", wid)
                continue
            vp = wdir / "viz.json"
            if not vp.exists():
                continue
            viz = json.loads(vp.read_text(encoding="utf-8"))
            stage = _stage(WorldBuilder.from_directory(str(wdir)), viz)
            mgr = EngineManager(wdir, store, shared_llm, viz, stage,
                                max_engines=max_engines, idle_timeout=idle_timeout, brain=brain, rate=rate)
            self.worlds[wid] = {"viz": viz, "stage": stage, "mgr": mgr, "meta": self._meta(wid, viz, stage)}
            logger.info("已装载世界 [%s]:%s", wid, viz.get("title", wid))

    @staticmethod
    def _meta(wid: str, viz: dict, stage: dict) -> dict:
        maps = viz.get("maps") or []
        cover = viz.get("cover") or (maps[0]["map"] if maps and maps[0].get("map") else None)  # 专属封面优先,否则回退首图
        if cover:
            cover = "/" + cover.lstrip("/")
        intro = viz.get("intro") or []
        return {
            "id": wid,
            "title": (viz.get("title") or wid).replace(" ", ""),
            "subtitle": viz.get("subtitle") or "",
            "blurb": intro[0] if intro else "",
            "cover": cover,
            "roster": len(stage.get("characters") or []),
            "places": len(stage.get("places") or []),
        }

    def mgr_for(self, sid: str):
        """会话 → (world_id, manager, stage);找不到返回 (None, None, None)。"""
        wid = self.store.session_world(sid)
        w = self.worlds.get(wid)
        return (wid, w["mgr"], w["stage"]) if w else (None, None, None)


class Handler(SimpleHTTPRequestHandler):
    def __init__(self, *a, **k):
        super().__init__(*a, directory=str(ROOT), **k)

    def log_message(self, *a):
        pass

    def end_headers(self):
        # 强制不缓存:HTML/JS/接口每次都取最新,杜绝"改了看不到"
        self.send_header("Cache-Control", "no-store, no-cache, must-revalidate")
        super().end_headers()

    def _sid(self) -> str | None:
        raw = self.headers.get("Cookie")
        if not raw:
            return None
        try:
            ck = SimpleCookie(raw)
            sid = ck["gsid"].value if "gsid" in ck else None
        except Exception:
            return None
        return sid if (sid and _store.session_exists(sid)) else None

    def _json(self, obj, code=200, set_sid: str | None = None):
        body = json.dumps(obj, ensure_ascii=False).encode("utf-8")
        self.send_response(code)
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        if set_sid:
            self.send_header("Set-Cookie",
                             f"gsid={set_sid}; Path=/; Max-Age=31536000; HttpOnly; SameSite=Lax")
        self.end_headers()
        self.wfile.write(body)

    def _redirect(self, to: str):
        self.send_response(302)
        self.send_header("Location", to)
        self.end_headers()

    def _body_json(self) -> dict:
        try:
            n = int(self.headers.get("Content-Length", 0))
            return json.loads(self.rfile.read(n) or "{}") if n else {}
        except Exception:
            return {}

    # ── GET ──
    def do_GET(self):
        path = urlparse(self.path).path
        if path == "/favicon.ico":
            self.send_response(204); self.end_headers(); return
        if path in ("/", "/index.html"):                # 落地页:选世界
            self._redirect(LANDING); return
        if path == "/api/worlds":                        # 平台:可玩世界列表(给落地页卡片)
            self._json({"worlds": [w["meta"] for w in _platform.worlds.values()]})
            return
        if path == "/api/auth":
            sid = self._sid()
            self._json({"authed": sid is not None, "world": _store.session_world(sid) if sid else None})
            return
        if path == "/api/state":
            sid = self._sid()
            if sid is None:
                self._json({"need_invite": True}, code=401); return
            wid, mgr, stage = _platform.mgr_for(sid)
            if mgr is None:
                self._json({"need_invite": True}, code=401); return
            ls = mgr.get(sid)                            # 不自动启动引擎(避免回来就烧 token)
            if ls is None:
                self._json({"suspended": True, "stage": stage, "world": wid}); return
            cost = ls.llm.stats() if ls.llm is not None else None
            with ls.lock:
                self._json({"stage": stage, "world": wid, "frame": ls.state["frame"],
                            "running": ls.state["running"], "step": ls.state["step"], "cost": cost})
            return
        if path == "/api/agent":
            sid = self._sid()
            wid, mgr, _ = _platform.mgr_for(sid) if sid else (None, None, None)
            if mgr is None:
                self._json({"need_invite": True}, code=401); return
            q = parse_qs(urlparse(self.path).query)
            self._json(mgr.ensure(sid).engine.agent_detail(q.get("name", [""])[0]))
            return
        super().do_GET()

    # ── POST ──
    def do_POST(self):
        p = urlparse(self.path)
        q = parse_qs(p.query)
        if p.path == "/api/activate":                      # 邀请码激活 → 绑定世界 + 种 cookie
            code = (self._body_json().get("code") or q.get("code", [""])[0])
            res = _store.activate_invite(code)
            if res is None:
                self._json({"ok": False, "error": "邀请码无效或已被使用"}, code=403)
            else:
                sid, world = res
                w = _platform.worlds.get(world)
                if w is None:
                    self._json({"ok": False, "error": "该邀请码所属世界暂不可用"}, code=503); return
                w["mgr"].ensure(sid)
                self._json({"ok": True, "world": world}, set_sid=sid)
            return
        sid = self._sid()
        wid, mgr, _ = _platform.mgr_for(sid) if sid else (None, None, None)
        if mgr is None:
            self._json({"need_invite": True}, code=401); return
        if p.path == "/api/heartbeat":
            mgr.heartbeat(sid); self._json({"ok": True}); return
        if p.path == "/api/suspend":
            mgr.suspend(sid); self._json({"ok": True}); return
        if p.path == "/api/resume_session":
            mgr.ensure(sid); self._json({"ok": True}); return
        ls = mgr.ensure(sid)
        eng = ls.engine
        if p.path == "/api/pause":
            ls.state["running"] = False; self._json({"running": False})
        elif p.path == "/api/resume":
            ls.state["running"] = True; self._json({"running": True})
        elif p.path == "/api/rate":
            r = float(q.get("x", ["30"])[0]); eng.set_base_rate(r); self._json({"rate": r})
        elif p.path == "/api/restart":
            ls._restart = True; eng.sched.stop(); self._json({"ok": True})
        elif p.path == "/api/event":
            self._json({"ok": True, "reached": eng.inject_event(q.get("desc", [""])[0], q.get("place", [""])[0])})
        elif p.path == "/api/weather":
            eng.set_weather(q.get("w", ["暴雨"])[0]); self._json({"ok": True})
        elif p.path == "/api/goal":
            eng.set_world_goal(q.get("text", [""])[0]); self._json({"ok": True})
        elif p.path == "/api/whisper":
            self._json({"ok": eng.whisper(q.get("name", [""])[0], q.get("text", [""])[0])})
        else:
            self.send_error(404)


def main() -> None:
    global _platform, _store
    import sys
    db = os.environ.get("GENESIS_DB", str(ROOT / "worlds" / "platform_sessions.db"))
    _store = SessionStore(db)

    if len(sys.argv) >= 2 and sys.argv[1] == "gen":        # 子命令:gen <world> <n>
        if len(sys.argv) < 3:
            print("用法:gen <world_id> [n]\n可用世界:" + "、".join(discover_worlds(str(ROOT / "worlds"))))
            return
        world = sys.argv[2]
        n = int(sys.argv[3]) if len(sys.argv) > 3 else 10
        codes = _store.gen_invites(n, world)
        print("\n".join(codes))
        print(f"\n✅ 为世界 [{world}] 生成 {n} 个邀请码 → {db}")
        return

    shared_llm = None
    if os.environ.get("GENESIS_BRAIN", "llm") != "stub":
        from genesis.llm.client import LiteLLMClient
        shared_llm = LiteLLMClient()
    _platform = Platform(
        _store, shared_llm,
        brain=os.environ.get("GENESIS_BRAIN", "llm"),
        rate=float(os.environ.get("GENESIS_RATE", "30")),
        max_engines=int(os.environ.get("GENESIS_MAX_ENGINES", "8")),
        idle_timeout=float(os.environ.get("GENESIS_IDLE_TIMEOUT", "90")))
    if not _platform.worlds:
        print("⚠️ 没有发现可玩世界(worlds/ 下需有合规世界包)"); return

    host = os.environ.get("GENESIS_HOST", "0.0.0.0")
    port = int(os.environ.get("GENESIS_PORT", "8000"))
    print(f"🌐 多世界平台启动 → http://{host}:{port}/  (世界:{'、'.join(_platform.worlds)})")
    ThreadingHTTPServer((host, port), Handler).serve_forever()


if __name__ == "__main__":
    main()
