/* App shell: sidebar, topbar, routing, theme, command palette */
const NAV = [
  { group: "Network", items: [
    { id: "dashboard", label: "Overview", icon: "dashboard" },
    { id: "map", label: "Network map", icon: "map" },
    { id: "machines", label: "Machines", icon: "machines", count: (s) => s.machines.length },
    { id: "routes", label: "Routes & exit", icon: "routes" },
  ]},
  { group: "Access", items: [
    { id: "users", label: "Users", icon: "users", count: (s) => s.users.length },
    { id: "keys", label: "Keys", icon: "key", count: (s) => s.preauthKeys.filter((k) => k.expiration > Date.now()).length + s.apiKeys.filter((k) => k.expiration > Date.now()).length },
    { id: "acl", label: "Access controls", icon: "acl" },
    { id: "dns", label: "DNS", icon: "dns" },
  ]},
];
const TITLES = { dashboard: "Overview", map: "Network map", machines: "Machines", routes: "Routes & exit nodes", users: "Users", keys: "Keys", acl: "Access controls", dns: "DNS", settings: "Settings", servers: "Backend" };

function App() {
  const s = useStore();
  const [route, setRoute] = useState(() => location.hash.slice(1) || "dashboard");
  const [theme, setTheme] = useState(() => localStorage.getItem("hs-theme") || "light");
  const [navOpen, setNavOpen] = useState(false);
  const [cmdk, setCmdk] = useState(false);
  const [machineFilter, setMachineFilter] = useState(null);
  const [welcome, setWelcome] = useState(false);

  useEffect(() => {
    const hasLive = (s.servers || []).some((sv) => sv.mode === "live");
    if (!hasLive && localStorage.getItem("hs.welcomeDismissed") !== "1") setWelcome(true);
  }, []);
  const dismissWelcome = () => { localStorage.setItem("hs.welcomeDismissed", "1"); setWelcome(false); };

  const go = useCallback((r, opts) => {
    setRoute(r); location.hash = r; setNavOpen(false);
    if (r === "machines" && opts?.filter) setMachineFilter(opts.filter); else if (r === "machines") setMachineFilter(null);
  }, []);

  useEffect(() => { document.documentElement.setAttribute("data-theme", theme); localStorage.setItem("hs-theme", theme); }, [theme]);
  useEffect(() => {
    const h = (e) => {
      if ((e.metaKey || e.ctrlKey) && e.key === "k") { e.preventDefault(); setCmdk((v) => !v); }
    };
    window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h);
  }, []);
  useEffect(() => {
    const onHash = () => setRoute(location.hash.slice(1) || "dashboard");
    window.addEventListener("hashchange", onHash); return () => window.removeEventListener("hashchange", onHash);
  }, []);

  const view = () => {
    switch (route) {
      case "dashboard": return <Dashboard go={go} />;
      case "map": return <NetworkMap />;
      case "machines": return <Machines key={machineFilter || "all"} initialFilter={machineFilter} />;
      case "routes": return <Routes />;
      case "users": return <Users />;
      case "keys": return <Keys />;
      case "acl": return <ACL />;
      case "dns": return <DNS />;
      case "settings": return <Settings theme={theme} setTheme={setTheme} />;
      case "servers": return <ServersPage />;
      default: return <Dashboard go={go} />;
    }
  };

  return (
    <div className={"app" + (navOpen ? " nav-open" : "")}>
      <aside className="sidebar">
        <div className="sb-brand">
          <div className="sb-logo" dangerouslySetInnerHTML={{ __html: window.Icon("lan", 18) }} />
          <div className="sb-brand-text"><b>Headscale</b><span>admin console</span></div>
        </div>
        <Menu align="left" width={244} trigger={
          <div className="sb-server">
            <span className={"dot-status " + (s.health.ok ? "online" : "warn")} title={s.health.ok ? "database connectivity ok" : "unreachable / degraded"} />
            <div className="sb-server-meta"><b>{(s.activeServer && s.activeServer.name) || "—"}</b><span>{s.serverInfo.url.replace(/^https?:\/\//, "")}</span></div>
            <span className={"badge " + (s.mode === "live" ? "online" : "accent")} style={{ height: 18, marginRight: 2 }}>{s.mode}</span>
            <Ic name="chevDown" size={14} style={{ color: "var(--text-faint)" }} />
          </div>
        }>
          <div className="menu-label">Headscale servers</div>
          {s.servers.map((sv) => (
            <MenuItem key={sv.id} icon={sv.id === s.activeId ? "check" : (sv.mode === "live" ? "globe" : "server")}
              onClick={() => HS.switchServer(sv.id)}><span style={{ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{sv.name}</span><span className={"badge " + (sv.mode === "live" ? "online" : "accent")} style={{ height: 18, marginLeft: 8 }}>{sv.mode}</span></MenuItem>
          ))}
          <div className="menu-sep" />
          <MenuItem icon="settings" onClick={() => go("servers")}>Manage servers…</MenuItem>
        </Menu>

        <nav className="sb-nav">
          {NAV.map((sec) => (
            <React.Fragment key={sec.group}>
              <div className="sb-section-label">{sec.group}</div>
              {sec.items.map((it) => (
                <div key={it.id} className={"sb-item" + (route === it.id ? " active" : "")} onClick={() => go(it.id)}>
                  <span dangerouslySetInnerHTML={{ __html: window.Icon(it.icon, 17) }} />
                  <span>{it.label}</span>
                  {it.count && <span className="count">{it.count(s)}</span>}
                </div>
              ))}
            </React.Fragment>
          ))}
        </nav>

        <div className="sb-foot">
          <div className={"sb-item" + (route === "servers" ? " active" : "")} onClick={() => go("servers")}>
            <span dangerouslySetInnerHTML={{ __html: window.Icon("server", 17) }} /><span>Backend</span>
            <span className={"badge " + (s.mode === "live" ? "online" : "accent")} style={{ height: 17, marginLeft: "auto" }}>{s.mode}</span>
          </div>
          <div className={"sb-item" + (route === "settings" ? " active" : "")} onClick={() => go("settings")}>
            <span dangerouslySetInnerHTML={{ __html: window.Icon("settings", 17) }} /><span>Settings</span>
          </div>
          <div className="sb-user">
            <span style={{ width: 28, height: 28, borderRadius: 8, background: "var(--surface-3)", display: "grid", placeItems: "center", color: s.mode === "live" ? "var(--online)" : "var(--accent)", flexShrink: 0 }}><Ic name={s.mode === "live" ? "key" : "server"} size={15} /></span>
            <div className="sb-user-meta"><b>{s.mode === "live" ? "API-key auth" : "Demo mode"}</b><span>{s.mode === "live" ? "full admin access" : "no authentication"}</span></div>
            <Menu trigger={<span className="copy-btn"><Ic name="dots" size={16} /></span>}>
              <MenuItem icon="book" onClick={() => window.open("https://headscale.net/stable/", "_blank")}>Headscale docs</MenuItem>
              <MenuItem icon="acl" onClick={() => go("acl")}>Policy reference</MenuItem>
            </Menu>
          </div>
        </div>
      </aside>

      {navOpen && <div className="scrim" onClick={() => setNavOpen(false)} />}

      <div className="main">
        <header className="topbar">
          <button className="btn icon sm ghost sb-toggle" onClick={() => setNavOpen(true)}><Ic name="layers" size={18} /></button>
          <div className="tb-title">
            <h1>{TITLES[route] || "Headscale"}</h1>
            <span className="crumb">{s.serverInfo.url.replace(/^https?:\/\//, "")} / {route}</span>
          </div>
          {s.loading && <span className="badge" style={{ height: 22 }}><Ic name="refresh" size={12} className="spin" />syncing</span>}
          <div className="tb-spacer" />
          <div className="tb-search" onClick={() => setCmdk(true)}>
            <Ic name="search" size={15} />
            <span className="label-txt">Search or jump to…</span>
            <span className="kbd">⌘K</span>
          </div>
          <button className="btn icon ghost" onClick={() => HS.refresh()} title="Refresh data from the control server"><Ic name="refresh" size={17} className={s.loading ? "spin" : ""} /></button>
          <button className="btn icon ghost" onClick={() => setTheme((t) => t === "dark" ? "light" : "dark")} title="Toggle theme">
            <Ic name={theme === "dark" ? "sun" : "moon"} size={17} />
          </button>
          <Menu trigger={<button className="btn icon ghost" style={{ position: "relative" }}><Ic name="bell" size={17} /><span style={{ position: "absolute", top: 7, right: 8, width: 6, height: 6, borderRadius: 99, background: "var(--accent)" }} /></button>}>
            <div className="menu-label">Notifications</div>
            <MenuItem icon="routes" onClick={() => go("routes")}>Route pending approval<span className="meta">34m</span></MenuItem>
            <MenuItem icon="clock" onClick={() => go("machines")}>2 keys expiring soon<span className="meta">2h</span></MenuItem>
            <MenuItem icon="plus" onClick={() => go("machines")}>ci-runner-02 joined<span className="meta">5m</span></MenuItem>
          </Menu>
        </header>

        <main className="content">
          {s.mode === "live" && !s.health.ok && (
            <div style={{ margin: "18px 30px 0" }}>
              <div style={{ display: "flex", gap: 11, alignItems: "center", padding: "12px 15px", background: "var(--danger-soft)", color: "var(--danger)", border: "1px solid color-mix(in oklab, var(--danger) 30%, transparent)", borderRadius: 10 }}>
                <Ic name="warn" size={18} />
                <div style={{ flex: 1, fontSize: 13 }}><b>Can't reach {s.serverInfo.url}.</b> {s.error || "Check the URL, API token, and that the server is reachable from this browser (CORS)."}</div>
                <button className="btn sm" onClick={() => HS.refresh()}><Ic name="refresh" size={14} />Retry</button>
                <button className="btn sm" onClick={() => go("servers")}>Manage</button>
              </div>
            </div>
          )}
          <div key={s.activeId} style={{ display: "contents" }}>{view()}</div>
        </main>
      </div>

      {cmdk && <CommandPalette go={go} close={() => setCmdk(false)} setTheme={setTheme} />}
      {welcome && <WelcomeModal onExplore={dismissWelcome} onAddServer={() => { dismissWelcome(); go("servers"); }} />}
    </div>
  );
}

function CommandPalette({ go, close, setTheme }) {
  const s = useStore();
  const [q, setQ] = useState("");
  const [active, setActive] = useState(0);
  const inputRef = useRef(null);
  useEffect(() => { inputRef.current?.focus(); }, []);

  const pages = NAV.flatMap((sec) => sec.items).map((it) => ({ kind: "page", id: it.id, label: it.label, icon: it.icon }));
  const machines = s.machines.map((m) => ({ kind: "machine", id: m.id, label: m.name, sub: m.ip4, icon: osIcon(m.kind, m.os) }));
  const actions = [
    { kind: "action", id: "reg", label: "Register machine", icon: "plus", run: () => go("machines") },
    { kind: "action", id: "key", label: "Generate pre-auth key", icon: "key", run: () => go("keys") },
    { kind: "action", id: "theme", label: "Toggle light / dark theme", icon: "moon", run: () => setTheme((t) => t === "dark" ? "light" : "dark") },
    { kind: "action", id: "acl", label: "Edit access controls", icon: "acl", run: () => go("acl") },
  ];

  const ql = q.toLowerCase();
  const fPages = pages.filter((p) => p.label.toLowerCase().includes(ql));
  const fActions = actions.filter((a) => a.label.toLowerCase().includes(ql));
  const fMachines = q ? machines.filter((m) => m.label.toLowerCase().includes(ql) || m.sub.includes(ql)).slice(0, 6) : [];
  const flat = [...fPages.map((x) => ({ ...x, _g: "Navigate" })), ...fActions.map((x) => ({ ...x, _g: "Actions" })), ...fMachines.map((x) => ({ ...x, _g: "Machines" }))];

  const choose = (item) => {
    if (!item) return;
    if (item.kind === "page") go(item.id);
    else if (item.kind === "action") item.run();
    else if (item.kind === "machine") go("machines");
    close();
  };

  const onKey = (e) => {
    if (e.key === "ArrowDown") { e.preventDefault(); setActive((a) => Math.min(a + 1, flat.length - 1)); }
    if (e.key === "ArrowUp") { e.preventDefault(); setActive((a) => Math.max(a - 1, 0)); }
    if (e.key === "Enter") { e.preventDefault(); choose(flat[active]); }
    if (e.key === "Escape") close();
  };

  let gi = -1;
  let lastG = null;
  return (
    <div className="cmdk-overlay" onMouseDown={(e) => e.target === e.currentTarget && close()}>
      <div className="cmdk">
        <div className="cmdk-input">
          <Ic name="search" size={18} />
          <input ref={inputRef} value={q} onChange={(e) => { setQ(e.target.value); setActive(0); }} onKeyDown={onKey} placeholder="Search machines, run actions, jump to a page…" />
          <span className="kbd">esc</span>
        </div>
        <div className="cmdk-list">
          {flat.length === 0 && <div className="empty" style={{ padding: 30 }}><div>No results for “{q}”</div></div>}
          {flat.map((item) => {
            gi++;
            const showG = item._g !== lastG; lastG = item._g;
            const idx = gi;
            return (
              <React.Fragment key={item.kind + item.id}>
                {showG && <div className="cmdk-group-label">{item._g}</div>}
                <div className={"cmdk-item" + (active === idx ? " active" : "")} onMouseEnter={() => setActive(idx)} onClick={() => choose(item)}>
                  <span dangerouslySetInnerHTML={{ __html: window.Icon(item.icon, 16) }} />
                  <span>{item.label}</span>
                  {item.sub && <span className="meta">{item.sub}</span>}
                  {item.kind === "page" && <span className="meta">↵</span>}
                </div>
              </React.Fragment>
            );
          })}
        </div>
      </div>
    </div>
  );
}

function Settings({ theme, setTheme }) {
  const s = useStore();
  return (
    <div className="page fade-up">
      <div className="section-head"><div><h2 style={{ fontSize: 19 }}>Settings</h2><div className="sub">Console preferences &amp; server configuration</div></div></div>
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14, alignItems: "start" }} className="dns-cols">
        <div className="card card-pad" style={{ display: "flex", flexDirection: "column", gap: 14 }}>
          <div className="section-head" style={{ margin: 0 }}><h2>Appearance</h2></div>
          <div style={{ display: "flex", gap: 10 }}>
            {["light", "dark"].map((t) => (
              <div key={t} onClick={() => setTheme(t)} style={{ flex: 1, cursor: "pointer", border: "1px solid " + (theme === t ? "var(--accent)" : "var(--border-strong)"), borderRadius: 10, padding: 12, background: theme === t ? "var(--accent-soft)" : "transparent" }}>
                <div style={{ height: 56, borderRadius: 7, background: t === "dark" ? "#141418" : "#fff", border: "1px solid var(--border)", marginBottom: 9, display: "flex", overflow: "hidden" }}>
                  <div style={{ width: "32%", background: t === "dark" ? "#1b1b21" : "#f1f1f4", borderRight: "1px solid " + (t === "dark" ? "#26262e" : "#e5e5ea") }} />
                  <div style={{ flex: 1, padding: 7 }}><div style={{ height: 6, width: "60%", background: "#8b5cf6", borderRadius: 3 }} /><div style={{ height: 5, width: "80%", background: t === "dark" ? "#26262e" : "#e5e5ea", borderRadius: 3, marginTop: 5 }} /></div>
                </div>
                <div style={{ display: "flex", alignItems: "center", gap: 7, fontSize: 13, fontWeight: 550, textTransform: "capitalize" }}><Ic name={t === "dark" ? "moon" : "sun"} size={15} />{t}</div>
              </div>
            ))}
          </div>
        </div>
        <div className="card card-pad" style={{ display: "flex", flexDirection: "column", gap: 12 }}>
          <div className="section-head" style={{ margin: 0 }}><h2>Server</h2></div>
          <div style={{ display: "flex", flexDirection: "column", gap: 10, fontSize: 13 }}>
            <InfoRow label="Control URL"><span className="mono" style={{ fontSize: 12 }}>{s.serverInfo.url}</span></InfoRow>
            <InfoRow label="Version"><span className="badge accent">{s.serverInfo.version}</span></InfoRow>
            <InfoRow label="IPv4 prefix"><span className="ip">{s.serverInfo.ipv4}</span></InfoRow>
            <InfoRow label="IPv6 prefix"><span className="ip">{s.serverInfo.ipv6}</span></InfoRow>
            <InfoRow label="Uptime"><span className="mono">{dur(s.serverInfo.uptime)}</span></InfoRow>
          </div>
        </div>
      </div>
    </div>
  );
}

/* ---- Headscale Servers: standalone multi-server management page ---- */
function ServersPage() {
  const s = useStore();
  const toast = useToast();
  const blank = { name: "", url: "https://", token: "", mode: "live", seed: "full" };
  const [editing, setEditing] = useState(null); // null | "new" | server id
  const [form, setForm] = useState(blank);

  const [io, setIo] = useState(null); // null | "export" | "import"
  const startEdit = (sv) => { setEditing(sv.id); setForm({ ...sv }); };
  const startNew = () => { setEditing("new"); setForm(blank); };
  const cancel = () => { setEditing(null); setForm(blank); };
  // ESC cancels the editor / closes the import-export panel
  useEffect(() => {
    const h = (e) => { if (e.key === "Escape") { if (io) setIo(null); else if (editing) cancel(); } };
    window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h);
  }, [editing, io]);
  const saveForm = () => {
    if (editing === "new") { HS.addServer(form); toast.push({ kind: "success", title: "Server added", msg: form.name }); }
    else { HS.updateServer(editing, form); toast.push({ kind: "success", title: "Server updated", msg: form.name }); }
    cancel();
  };

  return (
    <div className="page wide fade-up">
      <div className="section-head">
        <div>
          <h2 style={{ fontSize: 19 }}>Headscale Servers</h2>
          <div className="sub">Manage demo &amp; live control servers · test connectivity and authentication</div>
        </div>
        <div className="spacer" />
        {!editing && <>
          <button className="btn" onClick={() => setIo("import")}><Ic name="download" size={15} style={{ transform: "rotate(180deg)" }} />Import</button>
          <button className="btn" onClick={() => setIo("export")}><Ic name="download" size={15} />Export</button>
          <button className="btn primary" onClick={startNew}><Ic name="plus" size={15} />Add server</button>
        </>}
      </div>

      {io && <ServerIO mode={io} close={() => setIo(null)} toast={toast} />}

      {!editing && (
        <>
          <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
            {s.servers.map((sv) => (
              <div key={sv.id} className="card" style={{ padding: "13px 15px", display: "flex", alignItems: "center", gap: 13, borderColor: sv.id === s.activeId ? "var(--accent)" : "var(--border)", background: sv.id === s.activeId ? "var(--accent-soft)" : "var(--surface)" }}>
                <div style={{ width: 38, height: 38, borderRadius: 9, display: "grid", placeItems: "center", flexShrink: 0, background: "var(--surface-2)", color: sv.mode === "live" ? "var(--online)" : "var(--accent)" }}>
                  <Ic name={sv.mode === "live" ? "globe" : "server"} size={19} />
                </div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontWeight: 600, fontSize: 14, display: "flex", alignItems: "center", gap: 8 }}>
                    {sv.name}
                    <span className={"badge " + (sv.mode === "live" ? "online" : "accent")} style={{ height: 18 }}>{sv.mode}</span>
                    {sv.id === s.activeId && <span className="badge" style={{ height: 18 }}><span className="dot" style={{ background: "var(--accent)" }} />active</span>}
                  </div>
                  <div className="mono" style={{ fontSize: 12, color: "var(--text-faint)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                    {sv.url}{sv.mode === "live" ? (sv.token ? " · token set" : " · no token") : " · seed " + sv.seed}
                  </div>
                </div>
                {sv.id !== s.activeId
                  ? <button className="btn sm primary" onClick={() => { HS.switchServer(sv.id); toast.push({ kind: "success", title: "Switched server", msg: sv.name }); }}><Ic name="power" size={14} />Switch</button>
                  : <button className="btn sm" onClick={() => HS.refresh()}><Ic name="refresh" size={14} />Refresh</button>}
                <button className="btn icon sm ghost" title="Edit" onClick={() => startEdit(sv)}><Ic name="edit" size={15} /></button>
                <button className="btn icon sm ghost" title="Remove" disabled={s.servers.length <= 1} onClick={() => { HS.removeServer(sv.id); toast.push({ kind: "info", title: "Server removed", msg: sv.name }); }}><Ic name="trash" size={15} /></button>
              </div>
            ))}
          </div>
          <div style={{ marginTop: 14, display: "flex", gap: 9, color: "var(--text-dim)", fontSize: 12.5, padding: "11px 14px", background: "var(--surface-2)", border: "1px solid var(--border)", borderRadius: 10 }}>
            <Ic name="info" size={16} style={{ flexShrink: 0, marginTop: 1, color: "var(--accent)" }} />
            <div><b>Demo</b> servers run fully in-browser with rich mock data — no network needed. <b>Live</b> servers talk to a real Headscale API with a <span className="code-inline">Bearer</span> token; the server must send CORS headers for this page's origin. Server list &amp; tokens are stored in this browser only.</div>
          </div>
        </>
      )}

      {editing && <ServerEditor form={form} setForm={setForm} isNew={editing === "new"} onSave={saveForm} onCancel={cancel} />}
    </div>
  );
}

/* ---- export / import live-server configs (file + copy/paste) ---- */
function ServerIO({ mode, close, toast }) {
  const s = useStore();
  const liveCount = s.servers.filter((sv) => sv.mode === "live").length;
  const [text, setText] = useState(mode === "export" ? HS.exportServers(true) : "");
  const [copied, setCopied] = useState(false);
  const fileRef = useRef(null);

  const download = () => {
    const blob = new Blob([HS.exportServers(true)], { type: "application/json" });
    const a = document.createElement("a"); a.href = URL.createObjectURL(blob);
    a.download = "headscale-servers.json"; a.click(); URL.revokeObjectURL(a.href);
  };
  const onFile = (e) => {
    const f = e.target.files && e.target.files[0]; if (!f) return;
    const r = new FileReader(); r.onload = () => setText(String(r.result || "")); r.readAsText(f);
  };
  const doImport = () => {
    const res = HS.importServers(text);
    if (!res.ok) { toast.push({ kind: "error", title: "Import failed", msg: res.error }); return; }
    toast.push({ kind: res.added ? "success" : "info", title: "Import complete", msg: `${res.added} added${res.skipped ? `, ${res.skipped} skipped` : ""}` });
    close();
  };

  return (
    <Modal title={mode === "export" ? "Export live servers" : "Import live servers"} subtitle="JSON — file or copy & paste · demo servers are excluded" icon="server" size="lg" onClose={close}
      footer={mode === "export"
        ? <><div className="spacer" /><button className="btn" onClick={close}>Done</button></>
        : <><div className="spacer" /><button className="btn" onClick={close}>Cancel</button><button className="btn primary" disabled={!text.trim()} onClick={doImport}>Import</button></>}>
      {mode === "export" ? (
        <>
          {liveCount === 0 && <Banner cls="warn" icon="info">No live servers to export yet.</Banner>}
          <div style={{ display: "flex", gap: 9 }}>
            <button className="btn sm" onClick={() => { navigator.clipboard?.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 1200); }}><Ic name={copied ? "check" : "copy"} size={14} />{copied ? "Copied" : "Copy"}</button>
            <button className="btn sm" onClick={download}><Ic name="download" size={14} />Download .json</button>
          </div>
          <textarea className="input mono" readOnly value={text} style={{ height: 240, padding: 12, lineHeight: 1.55, resize: "vertical", whiteSpace: "pre" }} onFocus={(e) => e.target.select()} />
          <Banner cls="warn" icon="shield">This includes API keys in plaintext. Store the file securely and share it only over trusted channels.</Banner>
        </>
      ) : (
        <>
          <div style={{ display: "flex", gap: 9, alignItems: "center" }}>
            <button className="btn sm" onClick={() => fileRef.current && fileRef.current.click()}><Ic name="download" size={14} style={{ transform: "rotate(180deg)" }} />Choose file…</button>
            <input ref={fileRef} type="file" accept=".json,application/json" style={{ display: "none" }} onChange={onFile} />
            <span style={{ fontSize: 12, color: "var(--text-faint)" }}>or paste JSON below</span>
          </div>
          <textarea className="input mono" value={text} placeholder={'{ "servers": [ { "name": "prod", "url": "https://headscale.example.com", "token": "hskey-api-…" } ] }'} style={{ height: 220, padding: 12, lineHeight: 1.55, resize: "vertical", whiteSpace: "pre" }} onChange={(e) => setText(e.target.value)} />
          <Banner cls="warn" icon="info">Only <b>live</b> servers are imported (forced to live mode). Entries whose URL already exists are skipped.</Banner>
        </>
      )}
    </Modal>
  );
}

/* editor + connection test */
function ServerEditor({ form, setForm, isNew, onSave, onCancel }) {
  const [test, setTest] = useState(null);   // null | {running} | report
  // editing url/token invalidates a prior test result
  const set = (patch) => { if ("url" in patch || "token" in patch) setTest(null); setForm({ ...form, ...patch }); };

  const runTest = async () => {
    setTest({ running: true, checks: [] });
    try {
      const report = await HS.testServer({ url: form.url, token: form.token });
      setTest(report);
    } catch (e) {
      setTest({ ok: false, checks: [{ id: "err", label: "Test", status: "fail", detail: String(e && e.message || e) }] });
    }
  };

  const needsTest = form.mode === "live";
  const tested = test && !test.running && test.ok;

  // pre-test validation (live mode): name ≥3, valid https URL with hostname, plausible API key
  const nameOk = form.name.trim().length >= 3;
  const urlInfo = (() => {
    const v = (form.url || "").trim();
    if (!v) return { ok: false, msg: "" };
    let u; try { u = new URL(v); } catch (e) { return { ok: false, msg: "Not a valid URL" }; }
    if (u.protocol !== "https:" && u.protocol !== "http:") return { ok: false, msg: "Must be http:// or https://" };
    if (!u.hostname || !u.hostname.includes(".")) return { ok: false, msg: "Needs a full hostname" };
    return { ok: true, msg: "" };
  })();
  const urlOk = urlInfo.ok;
  const tokenOk = /^\S{8,}$/.test((form.token || "").trim());
  const preOk = nameOk && urlOk && tokenOk;          // gates the Test button (live)
  const canSave = nameOk && (form.mode === "demo" ? !!form.url.trim() : (preOk && tested));
  const ic = { pass: "check", warn: "warn", fail: "x" };
  const col = { pass: "var(--online)", warn: "var(--warn)", fail: "var(--danger)" };
  const fieldErr = (bad, show) => ({ borderColor: show && bad ? "var(--danger)" : "var(--border-strong)" });

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
    <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 14, alignItems: "start" }} className="acl-cols">
      {/* form */}
      <div className="card card-pad" style={{ display: "flex", flexDirection: "column", gap: 14 }}>
        <div className="section-head" style={{ margin: 0 }}><h2 style={{ whiteSpace: "nowrap" }}>{isNew ? "Add server" : "Edit server"}</h2></div>
        <div className="field"><label>Name</label><input className="input" autoFocus value={form.name} onChange={(e) => set({ name: e.target.value })} placeholder="my-headscale" style={fieldErr(!nameOk, form.name.length > 0)} />
          {form.name.length > 0 && !nameOk && <span className="hint" style={{ color: "var(--danger)" }}>At least 3 characters.</span>}</div>
        {isNew
          ? <div className="field"><label>Mode</label>
              <span className="badge online" style={{ alignSelf: "flex-start", height: 24 }}><span className="dot" />Live server</span>
              <span className="hint">Demo servers are provided by us — you can only add <b>live</b> servers here.</span></div>
          : <div className="field"><label>Mode</label>
              <span className={"badge " + (form.mode === "live" ? "online" : "accent")} style={{ alignSelf: "flex-start", height: 24 }}>{form.mode}</span></div>}
        <div className="field"><label>Control server URL</label><input className="input mono" value={form.url} onChange={(e) => set({ url: e.target.value })} placeholder="https://headscale.example.com" style={fieldErr(!urlOk, !!form.url.trim())} />
          {form.mode === "live" && form.url.trim() && !urlOk && <span className="hint" style={{ color: "var(--danger)" }}>{urlInfo.msg}</span>}</div>
        {form.mode === "live"
          ? <div className="field"><label>API key <span style={{ color: "var(--text-faint)", fontWeight: 400 }}>(Bearer token)</span></label>
              <input className="input mono" type="password" value={form.token} onChange={(e) => set({ token: e.target.value })} placeholder="hskey-api-…" style={fieldErr(!tokenOk, !!form.token.trim())} />
              {form.token.trim() && !tokenOk
                ? <span className="hint" style={{ color: "var(--danger)" }}>Enter your API key (8+ characters, no spaces).</span>
                : <span className="hint">Generated via <span className="code-inline">headscale apikeys create</span>. Stored in this browser only.</span>}</div>
          : <div className="field"><label>Demo dataset</label>
              <div className="seg" style={{ alignSelf: "flex-start" }}>
                <button className={form.seed === "full" ? "on" : ""} onClick={() => set({ seed: "full" })}>Full</button>
                <button className={form.seed === "lite" ? "on" : ""} onClick={() => set({ seed: "lite" })}>Lite</button>
              </div>
              <span className="hint">Full = the complete 17-node tailnet; Lite = a smaller fleet.</span></div>}
        {form.mode === "live" && <div style={{ display: "flex", gap: 8, padding: "9px 11px", background: "var(--surface-2)", border: "1px solid var(--border)", borderRadius: 8, fontSize: 11.5, color: "var(--text-dim)", lineHeight: 1.5 }}><Ic name="info" size={14} style={{ flexShrink: 0, marginTop: 1, color: "var(--accent)" }} /><div>This console is optimized for <b>Headscale v0.28.0</b>. Other versions may have a slightly different API and behave unexpectedly.</div></div>}
        <div style={{ display: "flex", gap: 9, marginTop: 2, alignItems: "center" }}>
          <button className="btn" onClick={onCancel}>Cancel</button>
          <div style={{ flex: 1 }} />
          {needsTest && !tested && <span style={{ fontSize: 11.5, color: "var(--text-faint)", textAlign: "right" }}>Pass a connection test to enable</span>}
          <button className="btn primary" disabled={!canSave} title={needsTest && !tested ? "Run a successful connection test first" : undefined} onClick={onSave}>{isNew ? "Add server" : "Save changes"}</button>
        </div>
      </div>

      {/* test / diagnostics */}
      <div className="card card-pad" style={{ display: "flex", flexDirection: "column", gap: 13 }}>
        <div className="section-head" style={{ margin: 0 }}><h2 style={{ whiteSpace: "nowrap" }}>Connection test</h2><div className="spacer" />
          <button className={"btn sm" + (needsTest && preOk && !tested ? " primary" : "")} disabled={form.mode !== "live" || !preOk || (test && test.running)} title={form.mode === "live" && !preOk ? "Fill a 3+ char name, an https URL, and a valid API key first" : undefined} onClick={runTest}>
            <Ic name={test && test.running ? "refresh" : "power"} size={14} className={test && test.running ? "spin" : ""} />{test && test.running ? "Testing…" : "Test connection"}
          </button>
        </div>

        {form.mode !== "live" && <div style={{ fontSize: 12.5, color: "var(--text-dim)" }}>Demo servers don't need a connection — they run locally. Switch to <b>Live</b> mode to test a real Headscale server.</div>}

        {form.mode === "live" && !test && (
          <div className="empty" style={{ padding: "22px 16px" }}>
            <Ic name="power" size={28} />
            <h3>Verify before you connect</h3>
            <div style={{ marginBottom: 12 }}>Provide these, then test reachability, TLS, CORS, <span className="code-inline">/health</span> and token auth:</div>
            <div style={{ display: "flex", flexDirection: "column", gap: 7, textAlign: "left", maxWidth: 280, margin: "0 auto" }}>
              {[["Name (3+ chars)", nameOk], ["Valid https:// URL with hostname", urlOk], ["API key (Bearer token)", tokenOk]].map(([t, ok]) => (
                <div key={t} style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 12.5, color: ok ? "var(--online)" : "var(--text-dim)" }}>
                  <Ic name={ok ? "check" : "x"} size={14} style={{ color: ok ? "var(--online)" : "var(--text-faint)" }} />{t}
                </div>
              ))}
            </div>
          </div>
        )}

        {form.mode === "live" && test && (
          <>
            {!test.running && (
              <div style={{ display: "flex", alignItems: "center", gap: 9, padding: "10px 12px", borderRadius: 9, background: test.ok ? "var(--online-soft)" : "var(--danger-soft)", color: test.ok ? "var(--online)" : "var(--danger)", fontSize: 13, fontWeight: 550 }}>
                <Ic name={test.ok ? "check" : "warn"} size={17} />
                {test.ok ? "Connection OK — authenticated successfully" : "Connection failed — see details below"}
                {test.elapsed != null && <span style={{ marginLeft: "auto", fontWeight: 400, opacity: 0.8, fontSize: 12 }}>{test.elapsed} ms</span>}
              </div>
            )}
            <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
              {test.checks.map((c, i) => (
                <div key={i} style={{ display: "flex", gap: 10, padding: "9px 11px", border: "1px solid var(--border)", borderRadius: 9 }}>
                  <Ic name={ic[c.status] || "info"} size={16} style={{ color: col[c.status] || "var(--text-dim)", flexShrink: 0, marginTop: 1 }} />
                  <div style={{ minWidth: 0 }}>
                    <div style={{ fontSize: 13, fontWeight: 550 }}>{c.label}</div>
                    <div style={{ fontSize: 12, color: "var(--text-dim)", lineHeight: 1.5 }}>{c.detail}</div>
                  </div>
                </div>
              ))}
              {test.running && <div style={{ fontSize: 12.5, color: "var(--text-dim)", display: "flex", gap: 7, alignItems: "center" }}><Ic name="refresh" size={14} className="spin" />Running checks…</div>}
            </div>
            {!test.running && !test.ok && (
              <div style={{ display: "flex", gap: 9, color: "var(--text-dim)", fontSize: 12, padding: "10px 12px", background: "var(--surface-2)", borderRadius: 9 }}>
                <Ic name="info" size={15} style={{ flexShrink: 0, marginTop: 1, color: "var(--accent)" }} />
                <div>Browsers can't distinguish DNS, TLS and CORS failures — they all surface as a generic network error. If <span className="code-inline">/health</span> is unreachable from here but works via <span className="code-inline">curl</span>, it's almost always missing CORS headers on the Headscale server.</div>
              </div>
            )}
          </>
        )}
      </div>
    </div>
    {form.mode === "live" && <CorsHelp url={form.url} />}
    </div>
  );
}

/* ---- reverse-proxy CORS setup help (live mode) ---- */
function CorsBox({ code }) {
  const [copied, setCopied] = useState(false);
  return (
    <div style={{ border: "1px solid var(--border)", borderRadius: 9, overflow: "hidden" }}>
      <div style={{ display: "flex", alignItems: "center", padding: "5px 8px 5px 12px", background: "var(--surface-2)", borderBottom: "1px solid var(--border)" }}>
        <span className="mono" style={{ fontSize: 11, color: "var(--text-faint)" }}>reverse-proxy config</span>
        <div style={{ flex: 1 }} />
        <button className="btn icon sm ghost" title="Copy" onClick={() => { navigator.clipboard?.writeText(code); setCopied(true); setTimeout(() => setCopied(false), 1200); }}><Ic name={copied ? "check" : "copy"} size={14} /></button>
      </div>
      <pre className="mono" style={{ margin: 0, padding: "12px 14px", fontSize: 11.5, lineHeight: 1.6, overflowX: "auto", color: "var(--text)", whiteSpace: "pre" }}>{code}</pre>
    </div>
  );
}

function CorsHelp({ url }) {
  const [open, setOpen] = useState(false);
  const [proxy, setProxy] = useState("nginx");
  let origin = "https://headscale.app";
  try { origin = window.location.origin && window.location.origin !== "null" ? window.location.origin : origin; } catch (e) {}

  const NGINX = `# /etc/nginx/conf.d/headscale.conf — must be reachable from the
# internet and served over HTTPS (e.g. with a Let's Encrypt cert).
server {
    listen 443 ssl;
    server_name headscale.example.com;

    ssl_certificate     /etc/letsencrypt/live/headscale.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/headscale.example.com/privkey.pem;

    location /api/ {
        # preflight: answered here, never forwarded to headscale
        if ($request_method = OPTIONS) {
            add_header Access-Control-Allow-Origin  "${origin}" always;
            add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, PATCH, OPTIONS" always;
            add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
            add_header Access-Control-Max-Age       600 always;
            add_header Content-Length 0;
            return 204;
        }

        add_header Access-Control-Allow-Origin "${origin}" always;

        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Host $server_name;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    # the rest of Headscale (DERP, OIDC callback, gRPC, …)
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Host $server_name;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $http_connection;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}`;

  const HAPROXY = `# /etc/haproxy/haproxy.cfg — TLS-terminating frontend with CORS.
frontend https_in
    bind *:443 ssl crt /etc/haproxy/certs/headscale.pem
    default_backend headscale

    # preflight: answer OPTIONS on /api/ directly with 204
    http-request return status 204 \\
        hdr "Access-Control-Allow-Origin"  "${origin}" \\
        hdr "Access-Control-Allow-Methods" "GET, POST, PUT, DELETE, PATCH, OPTIONS" \\
        hdr "Access-Control-Allow-Headers" "Authorization, Content-Type" \\
        hdr "Access-Control-Max-Age"       "600" \\
        if { path_beg /api/ } { method OPTIONS }

    # add the CORS header to every proxied API response
    http-response add-header Access-Control-Allow-Origin "${origin}" if { capture.req.uri -m beg /api/ }

backend headscale
    server hs 127.0.0.1:8080`;

  const APACHE = `# Apache 2.4 vhost — enable first:
#   a2enmod proxy proxy_http headers rewrite ssl
<VirtualHost *:443>
    ServerName headscale.example.com

    SSLEngine on
    SSLCertificateFile    /etc/letsencrypt/live/headscale.example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/headscale.example.com/privkey.pem

    # CORS headers on every response
    Header always set Access-Control-Allow-Origin  "${origin}"
    Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, PATCH, OPTIONS"
    Header always set Access-Control-Allow-Headers "Authorization, Content-Type"
    Header always set Access-Control-Max-Age       "600"

    # answer preflight OPTIONS with 204, never forward to headscale
    RewriteEngine On
    RewriteCond %{REQUEST_METHOD} OPTIONS
    RewriteRule ^(.*)$ $1 [R=204,L]

    ProxyPreserveHost On
    ProxyPass        / http://127.0.0.1:8080/
    ProxyPassReverse / http://127.0.0.1:8080/
</VirtualHost>`;

  const code = { nginx: NGINX, haproxy: HAPROXY, apache: APACHE }[proxy];

  return (
    <div className="card card-pad" style={{ display: "flex", flexDirection: "column", gap: open ? 13 : 0 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer" }} onClick={() => setOpen((v) => !v)}>
        <Ic name="globe" size={17} style={{ color: "var(--accent)" }} />
        <b style={{ fontSize: 13.5 }}>Enable CORS on your Headscale server</b>
        <span className="sub" style={{ color: "var(--text-faint)", fontSize: 12 }}>required for this browser app to reach the API</span>
        <div style={{ flex: 1 }} />
        <Ic name={open ? "chevDown" : "chevRight"} size={15} style={{ color: "var(--text-faint)" }} />
      </div>
      {open && (
        <>
          <p style={{ fontSize: 13, lineHeight: 1.6, color: "var(--text-dim)", margin: 0 }}>
            This console runs in your browser, so the browser's <b>same-origin policy</b> applies: Headscale must return CORS headers that allow this app's origin (<span className="code-inline">{origin}</span>), and answer <span className="code-inline">OPTIONS</span> preflight requests. Headscale itself doesn't send CORS headers — add them at your <b>reverse proxy</b>. The endpoint must be reachable from the public internet and served over <b>HTTPS</b>.
          </p>
          <div className="seg" style={{ alignSelf: "flex-start" }}>
            <button className={proxy === "nginx" ? "on" : ""} onClick={() => setProxy("nginx")}>nginx</button>
            <button className={proxy === "haproxy" ? "on" : ""} onClick={() => setProxy("haproxy")}>HAProxy</button>
            <button className={proxy === "apache" ? "on" : ""} onClick={() => setProxy("apache")}>Apache</button>
          </div>
          <CorsBox code={code} />
          <div style={{ display: "flex", gap: 9, color: "var(--text-dim)", fontSize: 12, padding: "10px 12px", background: "var(--surface-2)", borderRadius: 9 }}>
            <Ic name="shield" size={15} style={{ flexShrink: 0, marginTop: 1, color: "var(--accent)" }} />
            <div><b>Security:</b> these examples use <span className="code-inline">Access-Control-Allow-Origin: {origin}</span>. You can use <span className="code-inline">*</span> while testing, but pin it to this exact origin in production. CORS only governs browser access — it is not a substitute for the API token, which still authorizes every request. After editing, reload the proxy (e.g. <span className="code-inline">nginx -s reload</span>) and re-run <b>Test connection</b>.</div>
          </div>
        </>
      )}
    </div>
  );
}

/* ---- first-run welcome (public demo) — shown when no live server exists ---- */
function WelcomeModal({ onExplore, onAddServer }) {
  const steps = [
    { n: "1", t: "Open Backend", d: <>Click <b>Backend</b> in the sidebar (bottom-left).</> },
    { n: "2", t: "Add a server", d: <>Choose <b>Add server</b>, set mode to <b>Live</b>, and enter your control URL + an API key (<span className="code-inline">headscale apikeys create</span>).</> },
    { n: "3", t: "Test & connect", d: <>Run <b>Test connection</b> to verify reachability + auth, then <b>Connect</b> to manage your real tailnet.</> },
  ];
  return (
    <div className="cmdk-overlay" style={{ alignItems: "center" }} onMouseDown={(e) => e.target === e.currentTarget && onExplore()}>
      <div className="card fade-up" style={{ width: "min(560px, 94vw)", overflow: "hidden", boxShadow: "var(--shadow-lg)" }} onMouseDown={(e) => e.stopPropagation()}>
        <div style={{ padding: "26px 26px 20px", background: "linear-gradient(160deg, var(--accent-soft), transparent)" }}>
          <div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 14 }}>
            <div className="sb-logo" style={{ width: 38, height: 38 }} dangerouslySetInnerHTML={{ __html: window.Icon("lan", 22) }} />
            <div>
              <div style={{ fontWeight: 600, fontSize: 17, letterSpacing: "-0.01em" }}>Headscale Admin Console</div>
              <div style={{ fontSize: 12.5, color: "var(--text-dim)", fontFamily: "var(--font-mono)" }}>interactive demonstration</div>
            </div>
            <div style={{ flex: 1 }} />
            <span className="badge accent" style={{ height: 22 }}>demo</span>
          </div>
          <p style={{ fontSize: 14, lineHeight: 1.6, color: "var(--text-dim)" }}>
            You're exploring a fully interactive <b style={{ color: "var(--text)" }}>demo</b> of the admin UI for a self-hosted Headscale control server. Everything here — {" "}
            machines, users, keys, routes, DNS and the policy editor — runs <b style={{ color: "var(--text)" }}>entirely in your browser</b> on realistic mock data. Nothing is sent anywhere; refresh to reset.
          </p>
        </div>

        <div style={{ padding: "18px 26px 8px" }}>
          <div style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: ".06em", color: "var(--text-faint)", fontWeight: 600, marginBottom: 12 }}>Connect your own server (live mode)</div>
          <div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
            {steps.map((s) => (
              <div key={s.n} style={{ display: "flex", gap: 12, alignItems: "flex-start" }}>
                <div style={{ width: 24, height: 24, borderRadius: 99, background: "var(--accent)", color: "#fff", display: "grid", placeItems: "center", fontSize: 12, fontWeight: 600, flexShrink: 0 }}>{s.n}</div>
                <div style={{ fontSize: 13.5, lineHeight: 1.5 }}><b>{s.t}.</b> <span style={{ color: "var(--text-dim)" }}>{s.d}</span></div>
              </div>
            ))}
          </div>
          <div style={{ display: "flex", gap: 9, marginTop: 14, padding: "10px 12px", background: "var(--surface-2)", borderRadius: 9, fontSize: 12, color: "var(--text-dim)", lineHeight: 1.5 }}>
            <Ic name="shield" size={15} style={{ flexShrink: 0, marginTop: 1, color: "var(--accent)" }} />
            <div>Your server URL and API key are stored only in this browser. The server must allow this origin via CORS for live requests to succeed.</div>
          </div>
        </div>

        <div style={{ display: "flex", gap: 10, padding: "16px 26px 22px" }}>
          <button className="btn" style={{ flex: 1 }} onClick={onExplore}>Explore the demo</button>
          <button className="btn primary" style={{ flex: 1 }} onClick={onAddServer}><Ic name="plus" size={15} />Add a live server</button>
        </div>
      </div>
    </div>
  );
}

Object.assign(window, { App, CommandPalette, Settings, ServersPage, ServerEditor, ServerIO, WelcomeModal, CorsHelp, CorsBox });
