"""FastAPI application with WebRTC signaling and streaming-only voice pipeline."""

import asyncio
import logging
import os
from contextlib import asynccontextmanager
from pathlib import Path

from fastapi import FastAPI, WebSocket
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel

from roohai.pipeline import Rooh
from roohai.registry import registry

from .prepare import prepare_agent
from .streaming import handle_streaming
from .webrtc import cleanup_all, create_offer_response
from .wizard import router as wizard_router

# Configure logging for all server modules
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s [%(name)s] %(message)s",
    datefmt="%H:%M:%S",
)

logger = logging.getLogger(__name__)

# --- Pydantic models ---


class WebRTCOffer(BaseModel):
    sdp: str
    type: str = "offer"


class ModelSwapRequest(BaseModel):
    model_type: str  # "stt", "llm", or "tts"
    model_name: str


# --- App state ---

pipeline: Rooh | None = None

import roohai as _roohai_pkg

_PACKAGE_DIR = Path(_roohai_pkg.__file__).resolve().parent
CONFIG_PATH = _PACKAGE_DIR / "default_config.yaml"
FRONTEND_DIR = _PACKAGE_DIR / "frontend"


# --- Lifespan ---


@asynccontextmanager
async def lifespan(app: FastAPI):
    global pipeline
    logger.info("Loading config from %s", CONFIG_PATH)
    pipeline = Rooh(str(CONFIG_PATH))
    await asyncio.to_thread(pipeline.load)
    logger.info("Pipeline initialized and models loaded")

    # Auto-create and activate a Default agent on first boot
    from roohai.agents import list_agents, get_agent, save_agent
    from roohai.pipeline import register_all_models
    from datetime import datetime, timezone

    register_all_models()
    agents = list_agents()

    if not agents:
        logger.info("No agents found — creating Default agent")
        default_config = {
            "name": "Default",
            "created_at": datetime.now(timezone.utc).isoformat(),
            "status": "active",
            "pipeline": {
                "stt": "whisper-tiny",
                "llm": "bedrock-claude-haiku",
                "tts": "piper",
                "vad": "silero",
            },
            "models": {
                "whisper-tiny": {"class": "whisper-tiny", "type": "stt"},
                "bedrock-claude-haiku": {"class": "bedrock-claude-haiku", "type": "llm"},
                "piper": {"class": "piper", "type": "tts"},
                "silero": {"class": "silero", "type": "vad"},
            },
        }
        save_agent(default_config)
        agents = list_agents()

    # Find an active agent, or fall back to Default
    active_agent = None
    for a in agents:
        if a.get("status") == "active":
            active_agent = a
            break
    if not active_agent:
        active_agent = get_agent("Default") or agents[0]
        active_agent["status"] = "active"
        save_agent(active_agent)

    # Activate the agent into the pipeline.  If anything fails (bad hook,
    # missing model, etc.) deactivate the agent and fall back to Default.
    await _activate_agent(active_agent, pipeline)

    yield
    await cleanup_all()
    logger.info("Server shutdown complete")


async def _activate_agent(agent: dict, pipeline) -> None:
    """Validate and activate an agent's config into the pipeline.

    On any error (unresolvable hook, bad model, etc.) the agent is deactivated
    and the Default agent is activated instead.
    """
    import importlib
    from roohai.agents import get_agent, save_agent
    from roohai.pipeline import _register_from_config
    from roohai.secrets import get_agent_secrets

    agent_name = agent["name"]
    logger.info("Activating agent '%s'", agent_name)

    try:
        # 1. Validate hooks BEFORE touching the pipeline
        #    Add the agents directory to sys.path so agent-local hook
        #    modules (e.g. hooks.py alongside agent YAMLs) are importable.
        import sys
        from roohai.agents import AGENTS_DIR
        agents_dir_str = str(AGENTS_DIR)
        if agents_dir_str not in sys.path:
            sys.path.insert(0, agents_dir_str)

        hook_fn = None
        hook_path = agent.get("llm_hook")
        if hook_path:
            module_path, _, attr = hook_path.rpartition(".")
            mod = importlib.import_module(module_path)
            hook_fn = getattr(mod, attr)

        stream_hook_fn = None
        stream_hook_path = agent.get("llm_stream_hook")
        if stream_hook_path:
            module_path, _, attr = stream_hook_path.rpartition(".")
            mod = importlib.import_module(module_path)
            stream_hook_fn = getattr(mod, attr)

        # 2. Register and swap models
        secrets = get_agent_secrets(agent_name)
        models = agent.get("models", {})
        for model_name, model_cfg in models.items():
            model_secrets = secrets.get(model_name, {})
            if model_secrets:
                model_cfg.update(model_secrets)
        _register_from_config({"models": models})

        agent_pipeline = agent.get("pipeline", {})
        for model_type in ("stt", "tts", "llm", "vad"):
            model_name = agent_pipeline.get(model_type)
            if model_name:
                await asyncio.to_thread(pipeline.swap_model, model_type, model_name)

        # 3. Apply config
        pipeline.set_system_prompt(agent.get("system_prompt"))
        pipeline.llm_streaming = agent.get("llm_streaming", True)
        if hook_fn:
            pipeline.set_llm_hook(hook_fn, stream_hook=stream_hook_fn)
        else:
            pipeline.set_llm_hook(None)

        logger.info("Agent '%s' activated successfully", agent_name)

    except Exception as exc:
        logger.warning(
            "Failed to activate agent '%s': %s — falling back to Default",
            agent_name, exc,
        )
        agent["status"] = "inactive"
        save_agent(agent)

        fallback = get_agent("Default")
        if fallback and fallback["name"] != agent_name:
            fallback["status"] = "active"
            save_agent(fallback)
            await _activate_agent(fallback, pipeline)
        else:
            logger.warning("No Default agent to fall back to")


# --- FastAPI app ---

app = FastAPI(title="RoohAI", lifespan=lifespan)

_cors_raw = os.environ.get("ROOHAI_CORS_ORIGINS", "*")
_cors_origins = [o.strip() for o in _cors_raw.split(",") if o.strip()]

app.add_middleware(
    CORSMiddleware,
    allow_origins=_cors_origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

app.include_router(wizard_router)


# --- Health ---


@app.get("/api/health")
async def health():
    active = {}
    if pipeline:
        if pipeline._stt_name:
            active["stt"] = pipeline._stt_name
        if pipeline._tts_name:
            active["tts"] = pipeline._tts_name
        if pipeline._llm_name:
            active["llm"] = pipeline._llm_name
    return {"status": "ok", "models": active}


# --- WebSocket streaming ---


@app.websocket("/ws/voice-stream")
async def ws_voice_stream(websocket: WebSocket):
    await handle_streaming(websocket, pipeline)


# --- WebRTC signaling ---


@app.post("/api/webrtc/offer")
async def webrtc_offer(offer: WebRTCOffer):
    """Accept a WebRTC SDP offer and return an SDP answer."""
    from fastapi import HTTPException

    try:
        result = await create_offer_response(offer.sdp, offer.type, pipeline)
        return result
    except Exception:
        logger.exception("WebRTC offer failed")
        raise HTTPException(status_code=500, detail="WebRTC connection failed")


@app.post("/api/webrtc/ice")
async def webrtc_ice():
    """ICE candidate exchange endpoint (trickle ICE).

    For localhost connections, ICE candidates are typically gathered
    automatically during the offer/answer exchange. This endpoint
    exists for future use with STUN/TURN servers.
    """
    return {"status": "ok"}


# --- Model management ---


@app.post("/api/models/swap")
async def swap_model(req: ModelSwapRequest):
    """Hot-swap a model at runtime."""
    await asyncio.to_thread(pipeline.swap_model, req.model_type, req.model_name)
    active = {}
    if pipeline._stt_name:
        active["stt"] = pipeline._stt_name
    if pipeline._tts_name:
        active["tts"] = pipeline._tts_name
    if pipeline._llm_name:
        active["llm"] = pipeline._llm_name
    return {
        "status": "ok",
        "model_type": req.model_type,
        "model_name": req.model_name,
        "active_models": active,
    }


@app.get("/api/models")
async def list_models():
    """List available models and which are currently active."""
    return {
        "stt": registry.list_stt(),
        "tts": registry.list_tts(),
        "llm": registry.list_llm(),
        "active_stt": pipeline._stt_name,
        "active_tts": pipeline._tts_name,
        "active_llm": pipeline._llm_name,
    }


# --- WebSocket: Agent preparation ---


@app.websocket("/ws/agent-prepare")
async def ws_agent_prepare(websocket: WebSocket):
    """WebSocket endpoint for agent preparation with live progress."""
    agent_name = websocket.query_params.get("name", "")
    await websocket.accept()
    try:
        if not agent_name:
            await websocket.send_json({"status": "error", "message": "No agent name provided"})
            return

        from roohai.agents import get_agent
        from roohai.secrets import get_agent_secrets

        agent_config = get_agent(agent_name)
        if not agent_config:
            await websocket.send_json({"status": "error", "message": f"Agent '{agent_name}' not found"})
            return

        # Merge stored secrets into model configs so credential validation works
        secrets = get_agent_secrets(agent_name)
        for model_name, model_cfg in agent_config.get("models", {}).items():
            model_secrets = secrets.get(model_name, {})
            if model_secrets:
                model_cfg.update(model_secrets)

        await prepare_agent(agent_config, websocket)
    except Exception:
        logger.exception("Agent preparation WebSocket error")
        try:
            await websocket.send_json({"status": "error", "message": "Preparation failed"})
        except Exception:
            pass
    finally:
        try:
            await websocket.close()
        except Exception:
            pass


# --- Documentation ---

DOCS_DIR = _PACKAGE_DIR / "docs"

# Ordered navigation items: (slug, label, icon)
_NAV_ITEMS = [
    ("index", "Home", "&#9776;"),
    ("getting-started", "Getting Started", "&#9654;"),
    ("architecture", "Architecture", "&#9881;"),
    ("models", "Models", "&#9733;"),
    ("configuration", "Configuration", "&#9881;"),
    ("api", "API Reference", "&#8644;"),
    ("custom-models", "Custom Models", "&#10010;"),
    ("examples", "Examples", "&#128196;")
]

_DOCS_HTML_TEMPLATE = """\
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title} — RoohAI Docs</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5/github-markdown-light.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/styles/github.min.css">
<style>
  :root {{ --sidebar-w: 240px; --toc-w: 200px; --accent: #6c5ce7; --accent-light: #a29bfe; --bg: #fff; --sidebar-bg: #f8f9fa; --border: #e2e8f0; --text: #1a202c; --text-muted: #64748b; }}
  * {{ box-sizing: border-box; margin: 0; padding: 0; }}
  html {{ scroll-behavior: smooth; scroll-padding-top: 80px; }}
  body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: var(--bg); color: var(--text); display: flex; min-height: 100vh; }}

  /* ── Sidebar ── */
  .sidebar {{ width: var(--sidebar-w); position: fixed; top: 0; left: 0; bottom: 0; background: var(--sidebar-bg); border-right: 1px solid var(--border); overflow-y: auto; z-index: 10; display: flex; flex-direction: column; }}
  .sidebar-header {{ padding: 20px 16px 12px; border-bottom: 1px solid var(--border); }}
  .sidebar-header h2 {{ font-size: 16px; font-weight: 700; color: var(--accent); }}
  .sidebar-header p {{ font-size: 11px; color: var(--text-muted); margin-top: 2px; }}
  .sidebar-search {{ padding: 8px 12px; }}
  .sidebar-search input {{ width: 100%; padding: 7px 10px 7px 30px; border: 1px solid var(--border); border-radius: 6px; font-size: 13px; outline: none; background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E") 9px center no-repeat; }}
  .sidebar-search input:focus {{ border-color: var(--accent); box-shadow: 0 0 0 2px rgba(108,92,231,.15); }}
  .sidebar-nav {{ flex: 1; padding: 8px 0; }}
  .sidebar-nav a {{ display: flex; align-items: center; gap: 8px; padding: 7px 16px; font-size: 13px; color: var(--text-muted); text-decoration: none; border-left: 3px solid transparent; transition: all .15s; }}
  .sidebar-nav a:hover {{ color: var(--text); background: rgba(108,92,231,.05); }}
  .sidebar-nav a.active {{ color: var(--accent); border-left-color: var(--accent); background: rgba(108,92,231,.08); font-weight: 600; }}
  .sidebar-nav .nav-icon {{ font-size: 14px; width: 18px; text-align: center; }}

  /* ── Search results overlay ── */
  .search-results {{ display: none; position: absolute; left: 12px; right: 12px; top: 44px; background: #fff; border: 1px solid var(--border); border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,.12); max-height: 400px; overflow-y: auto; z-index: 100; }}
  .search-results.visible {{ display: block; }}
  .search-result {{ padding: 10px 14px; border-bottom: 1px solid #f1f5f9; cursor: pointer; }}
  .search-result:last-child {{ border-bottom: none; }}
  .search-result:hover {{ background: #f8f9fa; }}
  .search-result .sr-page {{ font-size: 11px; color: var(--accent); font-weight: 600; text-transform: uppercase; letter-spacing: .5px; }}
  .search-result .sr-heading {{ font-size: 13px; font-weight: 600; margin: 2px 0; }}
  .search-result .sr-snippet {{ font-size: 12px; color: var(--text-muted); line-height: 1.4; }}
  .search-result mark {{ background: #fef3c7; border-radius: 2px; padding: 0 1px; }}
  .search-empty {{ padding: 20px 14px; text-align: center; font-size: 13px; color: var(--text-muted); }}
  .search-hint {{ padding: 8px 14px; font-size: 11px; color: var(--text-muted); border-bottom: 1px solid #f1f5f9; }}

  /* ── Main content ── */
  .main {{ margin-left: var(--sidebar-w); flex: 1; display: flex; justify-content: center; }}
  .content-wrap {{ max-width: 780px; width: 100%; padding: 32px 40px 80px; }}
  .markdown-body {{ font-size: 15px; line-height: 1.7; }}
  .markdown-body h1 {{ font-size: 28px; margin-bottom: 8px; padding-bottom: 8px; border-bottom: 2px solid var(--accent); }}
  .markdown-body h2 {{ font-size: 22px; margin-top: 40px; margin-bottom: 12px; padding-bottom: 6px; border-bottom: 1px solid var(--border); }}
  .markdown-body h3 {{ font-size: 17px; margin-top: 28px; margin-bottom: 8px; }}
  .markdown-body pre {{ background: #f6f8fa; border-radius: 8px; padding: 16px; overflow-x: auto; border: 1px solid #e8ecf0; }}
  .markdown-body code {{ font-size: 13px; font-family: "SF Mono", "Fira Code", "Consolas", monospace; }}
  .markdown-body :not(pre) > code {{ background: #f0f2f5; padding: 2px 6px; border-radius: 4px; font-size: 12px; }}
  .markdown-body table {{ border-collapse: collapse; width: 100%; margin: 16px 0; }}
  .markdown-body th, .markdown-body td {{ border: 1px solid var(--border); padding: 8px 12px; text-align: left; font-size: 13px; }}
  .markdown-body th {{ background: #f8f9fa; font-weight: 600; }}
  .markdown-body blockquote {{ border-left: 3px solid var(--accent-light); padding: 8px 16px; margin: 16px 0; background: #fafafe; color: var(--text-muted); }}
  .markdown-body hr {{ border: none; border-top: 1px solid var(--border); margin: 32px 0; }}
  .markdown-body a {{ color: var(--accent); }}

  /* ── On-page TOC ── */
  .toc {{ position: fixed; right: max(20px, calc((100vw - var(--sidebar-w) - 780px) / 2 - var(--toc-w) - 20px)); top: 80px; width: var(--toc-w); max-height: calc(100vh - 120px); overflow-y: auto; font-size: 12px; }}
  .toc-title {{ font-size: 11px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: .8px; margin-bottom: 8px; }}
  .toc a {{ display: block; padding: 3px 0 3px 12px; color: var(--text-muted); text-decoration: none; border-left: 2px solid transparent; line-height: 1.5; transition: all .15s; }}
  .toc a:hover {{ color: var(--text); }}
  .toc a.active {{ color: var(--accent); border-left-color: var(--accent); }}
  .toc a.toc-h3 {{ padding-left: 24px; font-size: 11px; }}

  /* ── Mobile ── */
  .mobile-toggle {{ display: none; position: fixed; top: 12px; left: 12px; z-index: 20; background: var(--accent); color: #fff; border: none; border-radius: 8px; padding: 8px 12px; font-size: 16px; cursor: pointer; }}
  @media (max-width: 1100px) {{ .toc {{ display: none; }} }}
  @media (max-width: 768px) {{
    .sidebar {{ transform: translateX(-100%); transition: transform .2s; }}
    .sidebar.open {{ transform: translateX(0); }}
    .mobile-toggle {{ display: block; }}
    .main {{ margin-left: 0; }}
    .content-wrap {{ padding: 60px 16px 60px; }}
  }}
</style>
</head>
<body>

<button class="mobile-toggle" onclick="document.querySelector('.sidebar').classList.toggle('open')">&#9776;</button>

<aside class="sidebar">
  <div class="sidebar-header">
    <h2>RoohAI</h2>
    <p>Documentation</p>
  </div>
  <div class="sidebar-search" style="position:relative">
    <input type="text" id="search-input" placeholder="Search docs..." autocomplete="off">
    <div class="search-results" id="search-results"></div>
  </div>
  <nav class="sidebar-nav">
{nav_links}
  </nav>
</aside>

<div class="main">
  <div class="content-wrap">
    <article class="markdown-body" id="content"></article>
  </div>
</div>

<aside class="toc" id="toc"></aside>

<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11/build/highlight.min.js"></script>
<script>
(function() {{
  // ── Render markdown ──
  marked.setOptions({{
    highlight: function(code, lang) {{
      if (lang && hljs.getLanguage(lang)) return hljs.highlight(code, {{language: lang}}).value;
      return hljs.highlightAuto(code).value;
    }}
  }});
  var md = {markdown_json};
  document.getElementById("content").innerHTML = marked.parse(md);

  // ── Build table of contents from rendered headings ──
  var toc = document.getElementById("toc");
  var headings = document.querySelectorAll(".markdown-body h2, .markdown-body h3");
  if (headings.length > 1) {{
    var html = '<div class="toc-title">On this page</div>';
    headings.forEach(function(h, i) {{
      var id = "heading-" + i;
      h.id = id;
      var cls = h.tagName === "H3" ? "toc-h3" : "";
      html += '<a href="#' + id + '" class="' + cls + '">' + h.textContent + '</a>';
    }});
    toc.innerHTML = html;

    // Highlight active TOC item on scroll
    var tocLinks = toc.querySelectorAll("a");
    var observer = new IntersectionObserver(function(entries) {{
      entries.forEach(function(entry) {{
        if (entry.isIntersecting) {{
          tocLinks.forEach(function(a) {{ a.classList.remove("active"); }});
          var link = toc.querySelector('a[href="#' + entry.target.id + '"]');
          if (link) link.classList.add("active");
        }}
      }});
    }}, {{ rootMargin: "-80px 0px -70% 0px" }});
    headings.forEach(function(h) {{ observer.observe(h); }});
  }}

  // ── Search ──
  var searchInput = document.getElementById("search-input");
  var searchResults = document.getElementById("search-results");
  var searchTimeout = null;

  searchInput.addEventListener("input", function() {{
    clearTimeout(searchTimeout);
    var q = this.value.trim();
    if (q.length < 2) {{ searchResults.classList.remove("visible"); return; }}
    searchTimeout = setTimeout(function() {{
      fetch("/api/docs/search?q=" + encodeURIComponent(q))
        .then(function(r) {{ return r.json(); }})
        .then(function(data) {{
          if (!data.results || data.results.length === 0) {{
            searchResults.innerHTML = '<div class="search-empty">No results for "' + q.replace(/</g,"&lt;") + '"</div>';
          }} else {{
            var html = '<div class="search-hint">' + data.results.length + ' result' + (data.results.length > 1 ? 's' : '') + '</div>';
            data.results.forEach(function(r) {{
              html += '<div class="search-result" onclick="window.location.href=\\'' + r.url + '\\'">'
                + '<div class="sr-page">' + r.page + '</div>'
                + '<div class="sr-heading">' + r.heading + '</div>'
                + '<div class="sr-snippet">' + r.snippet + '</div>'
                + '</div>';
            }});
            searchResults.innerHTML = html;
          }}
          searchResults.classList.add("visible");
        }});
    }}, 200);
  }});

  // Close search on outside click
  document.addEventListener("click", function(e) {{
    if (!e.target.closest(".sidebar-search")) searchResults.classList.remove("visible");
  }});

  // Keyboard shortcut: "/" to focus search
  document.addEventListener("keydown", function(e) {{
    if (e.key === "/" && document.activeElement.tagName !== "INPUT") {{
      e.preventDefault();
      searchInput.focus();
    }}
  }});

  // Close mobile sidebar on link click
  document.querySelectorAll(".sidebar-nav a").forEach(function(a) {{
    a.addEventListener("click", function() {{ document.querySelector(".sidebar").classList.remove("open"); }});
  }});
}})();
</script>
</body>
</html>
"""


def _build_nav_links(active_page: str) -> str:
    """Build sidebar navigation HTML."""
    lines = []
    for slug, label, icon in _NAV_ITEMS:
        href = "/guide" if slug == "index" else f"/guide/{slug}"
        cls = ' class="active"' if slug == active_page else ""
        lines.append(f'    <a href="{href}"{cls}><span class="nav-icon">{icon}</span> {label}</a>')
    return "\n".join(lines)


def _build_search_index() -> list[dict]:
    """Build a search index from all markdown files. Each entry has page, heading, text, url."""
    import re
    index = []
    if not DOCS_DIR.exists():
        return index
    for md_file in sorted(DOCS_DIR.glob("*.md")):
        slug = md_file.stem
        page_title = slug.replace("-", " ").title()
        content = md_file.read_text(encoding="utf-8")
        # Get proper page title from first heading
        for line in content.splitlines():
            if line.startswith("# "):
                page_title = line[2:].strip()
                break
        # Split into sections by headings
        sections = re.split(r'^(#{1,3})\s+(.+)$', content, flags=re.MULTILINE)
        # sections = [pre, level, heading, text, level, heading, text, ...]
        if sections[0].strip():
            index.append({"page": page_title, "heading": page_title, "text": sections[0].strip(), "slug": slug, "anchor": ""})
        i = 1
        while i < len(sections) - 2:
            _level = sections[i]
            heading = sections[i + 1]
            text = sections[i + 2] if i + 2 < len(sections) else ""
            # Strip code blocks for cleaner search text
            clean = re.sub(r'```[\s\S]*?```', '', text)
            clean = re.sub(r'`[^`]+`', '', clean)
            clean = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', clean)
            clean = re.sub(r'[#*|_>-]', ' ', clean)
            clean = ' '.join(clean.split())
            if clean.strip():
                anchor = re.sub(r'[^a-z0-9]+', '-', heading.lower()).strip('-')
                index.append({"page": page_title, "heading": heading, "text": clean[:500], "slug": slug, "anchor": anchor})
            i += 3
    return index


# Pre-build search index (rebuilt on each server restart)
_search_index: list[dict] = []


@app.get("/guide/{page:path}", response_class=HTMLResponse)
@app.get("/guide", response_class=HTMLResponse)
async def serve_docs(page: str = "index"):
    global _search_index
    if not _search_index:
        _search_index = _build_search_index()

    page = page.strip("/") or "index"
    md_file = DOCS_DIR / f"{page}.md"
    if not md_file.exists() or not md_file.is_relative_to(DOCS_DIR):
        md_file = DOCS_DIR / "index.md"
        page = "index"

    content = md_file.read_text(encoding="utf-8")

    title = "Docs"
    for line in content.splitlines():
        if line.startswith("# "):
            title = line[2:].strip()
            break

    import json as _json
    # Escape </script> so it doesn't break the enclosing <script> tag
    markdown_json = _json.dumps(content).replace("</", "<\\/")
    html = _DOCS_HTML_TEMPLATE.format(
        title=title,
        markdown_json=markdown_json,
        nav_links=_build_nav_links(page),
    )
    return HTMLResponse(html)


@app.get("/api/docs")
async def list_docs():
    """List available documentation pages."""
    if not DOCS_DIR.exists():
        return JSONResponse({"pages": []})
    pages = []
    for f in sorted(DOCS_DIR.glob("*.md")):
        name = f.stem
        title = name.replace("-", " ").title()
        for line in f.read_text(encoding="utf-8").splitlines():
            if line.startswith("# "):
                title = line[2:].strip()
                break
        pages.append({"slug": name, "title": title, "url": f"/guide/{name}"})
    return JSONResponse({"pages": pages})


@app.get("/api/docs/search")
async def search_docs(q: str = ""):
    """Full-text search across all documentation pages."""
    global _search_index
    if not _search_index:
        _search_index = _build_search_index()

    q = q.strip().lower()
    if len(q) < 2:
        return JSONResponse({"results": []})

    import re
    terms = q.split()
    results = []
    for entry in _search_index:
        text_lower = entry["text"].lower()
        heading_lower = entry["heading"].lower()
        # All terms must appear in heading or text
        if not all(t in text_lower or t in heading_lower for t in terms):
            continue
        # Build snippet with highlights
        snippet = entry["text"][:200]
        for term in terms:
            pattern = re.compile(re.escape(term), re.IGNORECASE)
            snippet = pattern.sub(lambda m: f"<mark>{m.group()}</mark>", snippet)
        url = "/guide" if entry["slug"] == "index" else f"/guide/{entry['slug']}"
        results.append({
            "page": entry["page"],
            "heading": entry["heading"],
            "snippet": snippet,
            "url": url,
        })
        if len(results) >= 20:
            break
    return JSONResponse({"results": results})


# --- Static files & frontend ---

if FRONTEND_DIR.exists():
    app.mount("/static", StaticFiles(directory=str(FRONTEND_DIR)), name="static")


@app.get("/")
async def serve_index():
    index = FRONTEND_DIR / "index.html"
    if index.exists():
        return FileResponse(str(index))
    return {"message": "RoohAI API is running. Frontend not found."}
