import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import L from "leaflet";
import QRCode from "qrcode";
import { api } from "./api";
import { REL_COLORS } from "./colors";
import type { ColorBy, DefinitionPayload, GraphNode, Meta, NodePredicate, Quantifier, SuggestHit, TreeQuery, TreeResponse, ViewName } from "./types";
import { emptyPredicate } from "./types";
import { CanvasView } from "./viz";

const DEFAULT_RELS = [
  "inherited",
  "derived",
  "borrowed",
  "calque",
  "clipping",
  "back_formation",
  "abbreviation",
  "compound",
  "blend",
  "other",
  "cognate",
];

function useDebounced<T>(value: T, ms: number): T {
  const [v, setV] = useState(value);
  useEffect(() => {
    const t = setTimeout(() => setV(value), ms);
    return () => clearTimeout(t);
  }, [value, ms]);
  return v;
}

function Multi({
  options,
  value,
  onChange,
  placeholder,
}: {
  options: { key: string; label: string }[];
  value: string[];
  onChange: (v: string[]) => void;
  placeholder: string;
}) {
  const [q, setQ] = useState("");
  const filtered = useMemo(() => {
    const n = q.trim().toLowerCase();
    return options.filter((o) => !n || o.label.toLowerCase().includes(n) || o.key.toLowerCase().includes(n)).slice(0, 80);
  }, [options, q]);
  return (
    <div>
      <input className="field-input" value={q} placeholder={placeholder} onChange={(e) => setQ(e.target.value)} />
      <div className="checklist">
        {filtered.map((o) => (
          <label key={o.key}>
            <input
              type="checkbox"
              checked={value.includes(o.key)}
              onChange={() => onChange(value.includes(o.key) ? value.filter((x) => x !== o.key) : [...value, o.key])}
            />
            {o.label}
          </label>
        ))}
      </div>
    </div>
  );
}

function PredicateEditor({
  meta,
  value,
  onChange,
  extra,
  showPopularity = false,
}: {
  meta: Meta;
  value: NodePredicate;
  onChange: (p: NodePredicate) => void;
  extra?: ReactNode;
  showPopularity?: boolean;
}) {
  const langOpts = useMemo(
    () => meta.languages.slice(0, 2500).map((l) => ({ key: l.key, label: `${l.display || l.key} (${l.nodes})` })),
    [meta]
  );
  const famOpts = useMemo(() => meta.families.map((f) => ({ key: f.name, label: `${f.name} (${f.nodes})` })), [meta]);
  return (
    <div>
      <label className="field">Languages</label>
      <Multi options={langOpts} value={value.languages} onChange={(languages) => onChange({ ...value, languages })} placeholder="Filter languages" />
      <label className="field">Families</label>
      <Multi options={famOpts} value={value.families} onChange={(families) => onChange({ ...value, families })} placeholder="Filter families" />
      <label className="field">Macroarea</label>
      <div className="rel-grid">
        {meta.macroareas.map((m) => (
          <label key={m}>
            <input
              type="checkbox"
              checked={value.macroareas.includes(m)}
              onChange={() =>
                onChange({
                  ...value,
                  macroareas: value.macroareas.includes(m) ? value.macroareas.filter((x) => x !== m) : [...value.macroareas, m],
                })
              }
            />
            {m}
          </label>
        ))}
      </div>
      <label className="field">Term contains</label>
      <input className="field-input" value={value.term_contains} onChange={(e) => onChange({ ...value, term_contains: e.target.value })} />
      <label className="field">
        <input type="checkbox" checked={value.require_coords} onChange={(e) => onChange({ ...value, require_coords: e.target.checked })} /> Mapped coordinates
      </label>
      <label className="field">Has phonemes (comma IPA)</label>
      <input
        className="field-input"
        value={value.phonemes_have.join(" ")}
        onChange={(e) => onChange({ ...value, phonemes_have: e.target.value.split(/[\s,]+/).filter(Boolean) })}
        placeholder="θ ʃ"
      />
      <label className="field">Lacks phonemes</label>
      <input
        className="field-input"
        value={value.phonemes_lack.join(" ")}
        onChange={(e) => onChange({ ...value, phonemes_lack: e.target.value.split(/[\s,]+/).filter(Boolean) })}
      />
      <label className="field">Tone</label>
      <select
        value={value.require_tone === null ? "" : value.require_tone ? "yes" : "no"}
        onChange={(e) => onChange({ ...value, require_tone: e.target.value === "" ? null : e.target.value === "yes" })}
      >
        <option value="">any</option>
        <option value="yes">has tone</option>
        <option value="no">no tone</option>
      </select>
      {showPopularity && (
        <>
          <label className="field">Popularity in language</label>
          <p style={{ color: "var(--muted)", fontSize: 12, margin: "0 0 6px" }}>
            Modern lects via wordfreq (Zipf); fallback ranked lists for extra languages. Historical forms usually have no score.
          </p>
          <label className="field">Top N most frequent</label>
          <select
            value={value.max_rank ?? ""}
            onChange={(e) => onChange({ ...value, max_rank: e.target.value === "" ? null : Number(e.target.value) })}
          >
            <option value="">any</option>
            <option value={100}>top 100</option>
            <option value={1000}>top 1,000</option>
            <option value={10000}>top 10,000</option>
            <option value={100000}>top 100,000</option>
            <option value={1000000}>top 1,000,000</option>
          </select>
          <label className="field">
            Min Zipf {value.min_zipf != null ? value.min_zipf.toFixed(1) : "off"}
          </label>
          <input
            type="range"
            min={0}
            max={70}
            step={1}
            value={value.min_zipf == null ? 0 : Math.round(value.min_zipf * 10)}
            onChange={(e) => {
              const raw = Number(e.target.value);
              onChange({ ...value, min_zipf: raw <= 0 ? null : raw / 10 });
            }}
          />
          <p style={{ color: "var(--muted)", fontSize: 12, margin: "4px 0 0" }}>
            Zipf ≈ 3 rare · 4 uncommon · 5 everyday · 6+ very common. 0 = no minimum.
          </p>
        </>
      )}
      <label className="field">WALS feature</label>
      <select
        value={value.wals[0]?.feature_id || ""}
        onChange={(e) => {
          const id = e.target.value;
          onChange({ ...value, wals: id ? [{ feature_id: id, values: [] }] : [] });
        }}
      >
        <option value="">none</option>
        {meta.wals_features.map((f) => (
          <option key={f.id} value={f.id}>
            {f.id} {f.name}
          </option>
        ))}
      </select>
      {value.wals[0] && (
        <Multi
          options={(meta.wals_features.find((f) => f.id === value.wals[0].feature_id)?.values || []).map((v) => ({
            key: String(v.value),
            label: v.label,
          }))}
          value={value.wals[0].values.map(String)}
          onChange={(vals) => onChange({ ...value, wals: [{ ...value.wals[0], values: vals.map(Number) }] })}
          placeholder="Allowed values"
        />
      )}
      <label className="field">
        <input type="checkbox" checked={value.keep_unknown} onChange={(e) => onChange({ ...value, keep_unknown: e.target.checked })} /> Keep languages missing this metadata
      </label>
      {extra}
    </div>
  );
}

export default function App() {
  const [meta, setMeta] = useState<Meta | null>(null);
  const [q, setQ] = useState("");
  const [hits, setHits] = useState<SuggestHit[]>([]);
  const [searchOpen, setSearchOpen] = useState(false);
  const [activeHit, setActiveHit] = useState(0);
  const [term, setTerm] = useState("");
  const [lang, setLang] = useState("");
  const [view, setView] = useState<ViewName>("tree");
  const [colorBy, setColorBy] = useState<ColorBy>("relation");
  const [relations, setRelations] = useState<string[]>(DEFAULT_RELS);
  const [conf, setConf] = useState(0.7);
  const [depth, setDepth] = useState(3);
  const [leaf, setLeaf] = useState<NodePredicate>(emptyPredicate());
  const [pathNode, setPathNode] = useState<NodePredicate>(emptyPredicate());
  const [pathMatch, setPathMatch] = useState<Quantifier>("any");
  const [pathRelAny, setPathRelAny] = useState<string[]>([]);
  const [pathRelNone, setPathRelNone] = useState<string[]>([]);
  const [expand, setExpand] = useState<string[]>([]);
  const [tree, setTree] = useState<TreeResponse | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [selected, setSelected] = useState<GraphNode | null>(null);
  const [detail, setDetail] = useState<Record<string, unknown> | null>(null);
  const [definition, setDefinition] = useState<DefinitionPayload | null>(null);
  const [definitionLoading, setDefinitionLoading] = useState(false);
  const [filterTab, setFilterTab] = useState<"graph" | "leaf" | "path">("graph");
  const [mobilePanel, setMobilePanel] = useState<"none" | "filters" | "info">("none");
  const [qr, setQr] = useState<string | null>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const viewRef = useRef<CanvasView | null>(null);
  const mapRef = useRef<HTMLDivElement>(null);
  const mapObj = useRef<L.Map | null>(null);
  const searchRef = useRef<HTMLDivElement>(null);
  const cameraKeyRef = useRef<string>("");
  const dq = useDebounced(q, 80);

  useEffect(() => {
    api.meta().then(setMeta).catch((e) => setError(String(e)));
  }, []);

  useEffect(() => {
    const params = new URLSearchParams(window.location.search);
    const t = params.get("q");
    const l = params.get("lang");
    if (t && l) {
      setTerm(t);
      setLang(l);
      setQ(t);
    }
    if (params.get("view")) setView(params.get("view") as ViewName);
    if (params.get("depth")) setDepth(Number(params.get("depth")));
  }, []);

  useEffect(() => {
    if (!dq) {
      setHits(searchOpen && meta ? meta.examples : []);
      return;
    }
    let alive = true;
    api.suggest(dq).then((r) => {
      if (alive) {
        setHits(r.hits);
        setActiveHit(0);
      }
    });
    return () => {
      alive = false;
    };
  }, [dq, meta, searchOpen]);

  useEffect(() => {
    if (!searchOpen) return;
    const onPointerDown = (e: PointerEvent) => {
      if (searchRef.current && !searchRef.current.contains(e.target as Node)) {
        setSearchOpen(false);
      }
    };
    document.addEventListener("pointerdown", onPointerDown);
    return () => document.removeEventListener("pointerdown", onPointerDown);
  }, [searchOpen]);

  const query: TreeQuery | null = useMemo(() => {
    if (!term || !lang) return null;
    return {
      term,
      lang,
      edges: { relations, min_confidence: conf, max_depth: depth, max_visit: 50000 },
      leaf,
      path: {
        node: pathNode,
        quantifier: pathMatch,
        exactly_k: 1,
        relations_any: pathRelAny,
        relations_none: pathRelNone,
        apply_to_root: false,
      },
      expand,
      cluster_threshold: 28,
      max_payload: 2500,
      color_by: colorBy,
    };
  }, [term, lang, relations, conf, depth, leaf, pathNode, pathMatch, pathRelAny, pathRelNone, expand, colorBy]);

  useEffect(() => {
    if (!query) return;
    const ac = new AbortController();
    setLoading(true);
    setError(null);
    api
      .tree(query, ac.signal)
      .then((r) => {
        setTree(r);
        setLoading(false);
        const url = new URL(window.location.href);
        url.searchParams.set("q", query.term);
        url.searchParams.set("lang", query.lang);
        url.searchParams.set("view", view);
        url.searchParams.set("depth", String(depth));
        history.replaceState(null, "", url);
      })
      .catch((e) => {
        if (e.name !== "AbortError") {
          setError(String(e));
          setLoading(false);
        }
      });
    return () => ac.abort();
  }, [query, view, depth]);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas || (view !== "tree" && view !== "radial")) {
      if (viewRef.current) {
        viewRef.current.destroy();
        viewRef.current = null;
      }
      return;
    }
    if (viewRef.current?.canvas !== canvas) {
      viewRef.current?.destroy();
      viewRef.current = new CanvasView(canvas);
    }
    const cv = viewRef.current;
    cv.onSelect = (n) => {
      if (n.kind === "cluster") return;
      setSelected(n);
      setMobilePanel("info");
    };
    cv.onExpand = (key) => setExpand((e) => (e.includes(key) ? e : [...e, key]));
    if (tree?.nodes) {
      const camKey = `${term}|${lang}|${view}`;
      const resetCamera = cameraKeyRef.current !== camKey;
      cameraKeyRef.current = camKey;
      cv.setData(tree.nodes, view === "radial" ? "radial" : "tree", colorBy, resetCamera);
    }
  }, [tree, view, colorBy, term, lang]);

  useEffect(() => {
    viewRef.current?.setSelected(selected ? String(selected.id) : null);
  }, [selected]);

  useEffect(() => {
    return () => {
      viewRef.current?.destroy();
      viewRef.current = null;
    };
  }, []);

  useEffect(() => {
    if (!selected || selected.kind !== "word") {
      setDetail(null);
      setDefinition(null);
      setDefinitionLoading(false);
      return;
    }
    let alive = true;
    api
      .node(selected.term, selected.lang)
      .then((d) => {
        if (alive) setDetail(d);
      })
      .catch(() => {
        if (alive) setDetail(null);
      });
    setDefinitionLoading(true);
    setDefinition(null);
    api
      .define(selected.term, selected.lang, selected.iso_639_3)
      .then((d) => {
        if (alive) setDefinition(d);
      })
      .catch(() => {
        if (alive) setDefinition({ term: selected.term, senses: [], error: "fetch_failed" });
      })
      .finally(() => {
        if (alive) setDefinitionLoading(false);
      });
    return () => {
      alive = false;
    };
  }, [selected]);

  useEffect(() => {
    if (view !== "map" || !mapRef.current || !tree) return;
    if (!mapObj.current) {
      mapObj.current = L.map(mapRef.current, { zoomControl: true }).setView([30, 10], 2);
      L.tileLayer("https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png", {
        attribution: "&copy; OSM &copy; CARTO",
      }).addTo(mapObj.current);
    }
    const map = mapObj.current;
    map.eachLayer((layer) => {
      if (layer instanceof L.CircleMarker) map.removeLayer(layer);
    });
    const groups = new Map<string, { lat: number; lon: number; words: GraphNode[] }>();
    for (const n of tree.nodes) {
      if (n.kind !== "word" || n.latitude == null || n.longitude == null) continue;
      const key = n.lang;
      const g = groups.get(key) || { lat: n.latitude, lon: n.longitude, words: [] };
      g.words.push(n);
      groups.set(key, g);
    }
    for (const [key, g] of groups) {
      L.circleMarker([g.lat, g.lon], {
        radius: Math.min(18, 4 + Math.sqrt(g.words.length) * 2),
        color: REL_COLORS.inherited,
        fillOpacity: 0.7,
      })
        .bindPopup(`<b>${g.words[0].lang_display || key}</b><br/>${g.words.length} word(s)<br/>${g.words.slice(0, 8).map((w) => w.term).join(", ")}`)
        .addTo(map);
    }
    setTimeout(() => map.invalidateSize(), 50);
  }, [view, tree]);

  const pick = (hit: SuggestHit) => {
    setTerm(hit.term);
    setLang(hit.lang);
    setQ(hit.term);
    setHits([]);
    setSearchOpen(false);
    setExpand([]);
    setSelected(null);
    setMobilePanel("none");
  };

  const chipCount =
    leaf.languages.length +
    leaf.families.length +
    (leaf.max_rank ? 1 : 0) +
    (leaf.min_zipf != null ? 1 : 0) +
    pathNode.languages.length +
    pathNode.families.length +
    pathRelAny.length +
    (relations.length !== DEFAULT_RELS.length ? 1 : 0);

  const words = tree?.nodes.filter((n) => n.kind === "word") || [];

  const showQr = async () => {
    setQr(await QRCode.toDataURL(window.location.href, { margin: 1, width: 360 }));
  };

  return (
    <div className="app">
      <header className="topbar">
        <div className="brand">
          <strong>Reverse Etymology</strong>
          <span>Words derived from a given etymon</span>
        </div>
        <div className="search-wrap" ref={searchRef}>
          <span className="search-icon">⌕</span>
          <input
            value={q}
            placeholder="Search a word — mater, *méh₂tēr, caput…"
            onChange={(e) => {
              setQ(e.target.value);
              setSearchOpen(true);
            }}
            onKeyDown={(e) => {
              if (e.key === "ArrowDown") setActiveHit((i) => Math.min(hits.length - 1, i + 1));
              if (e.key === "ArrowUp") setActiveHit((i) => Math.max(0, i - 1));
              if (e.key === "Enter" && hits[activeHit]) pick(hits[activeHit]);
              if (e.key === "Escape") setSearchOpen(false);
            }}
            onFocus={() => {
              setSearchOpen(true);
              if (!q && meta) setHits(meta.examples);
            }}
          />
          {searchOpen && hits.length > 0 && (
            <div className="suggest">
              {hits.map((h, i) => (
                <button key={`${h.term}-${h.lang}-${i}`} className={i === activeHit ? "active" : ""} type="button" onClick={() => pick(h)}>
                  <span className="term">{h.term}</span>
                  <span>{h.lang_display || h.lang}</span>
                  <span className="meta">{h.child_count} derived</span>
                </button>
              ))}
            </div>
          )}
        </div>
        <button type="button" className="ghost filters-btn" onClick={() => setMobilePanel(mobilePanel === "filters" ? "none" : "filters")}>
          Filters{chipCount ? ` ${chipCount}` : ""}
        </button>
      </header>
      <div className="toolbar">
        <div className="seg">
          {(["tree", "radial", "map", "table", "stats"] as ViewName[]).map((v) => (
            <button key={v} className={view === v ? "on" : ""} onClick={() => setView(v)}>
              {v}
            </button>
          ))}
        </div>
        <select value={colorBy} onChange={(e) => setColorBy(e.target.value as ColorBy)}>
          <option value="relation">Color: relation</option>
          <option value="family">Color: family</option>
          <option value="confidence">Color: confidence</option>
          <option value="depth">Color: depth</option>
          <option value="macroarea">Color: area</option>
        </select>
        {term && (
          <span className="chip">
            <b>{term}</b> {lang}
          </span>
        )}
        {leaf.languages.map((l) => (
          <button key={l} className="chip" onClick={() => setLeaf({ ...leaf, languages: leaf.languages.filter((x) => x !== l) })}>
            leaf:{l} ×
          </button>
        ))}
        {pathNode.languages.map((l) => (
          <button key={l} className="chip" onClick={() => setPathNode({ ...pathNode, languages: pathNode.languages.filter((x) => x !== l) })}>
            path:{l} ×
          </button>
        ))}
        <span style={{ flex: 1 }} />
        <button className="ghost" onClick={showQr}>
          Phone
        </button>
        {query && (
          <button className="ghost" onClick={() => api.exportUrl(query)}>
            CSV
          </button>
        )}
      </div>
      <div className="workspace">
        {mobilePanel !== "none" && (
          <button type="button" className="sheet-backdrop" aria-label="Close panel" onClick={() => setMobilePanel("none")} />
        )}
        <aside className={`panel ${mobilePanel === "filters" ? "open-sheet" : ""}`}>
          <div className="sheet-grab">
            <span />
            <strong>Filters</strong>
            <button type="button" className="ghost sheet-close" onClick={() => setMobilePanel("none")}>
              Close
            </button>
          </div>
          <div className="sheet-body">
          {meta && (
            <>
              <div className="tabs">
                <button type="button" className={filterTab === "graph" ? "on" : ""} onClick={() => setFilterTab("graph")}>
                  Graph
                </button>
                <button type="button" className={filterTab === "leaf" ? "on" : ""} onClick={() => setFilterTab("leaf")}>
                  Leaves
                </button>
                <button type="button" className={filterTab === "path" ? "on" : ""} onClick={() => setFilterTab("path")}>
                  Path
                </button>
              </div>
              {filterTab === "graph" && (
                <div>
                  <p style={{ color: "var(--muted)", fontSize: 13 }}>
                    Construction filters apply to every hop before leaf/path tests.
                  </p>
                  <label className="field">Max depth {depth}</label>
                  <input type="range" min={1} max={8} value={depth} onChange={(e) => setDepth(Number(e.target.value))} />
                  <label className="field">Min confidence {conf.toFixed(2)}</label>
                  <input type="range" min={70} max={100} value={conf * 100} onChange={(e) => setConf(Number(e.target.value) / 100)} />
                  <label className="field">Relations</label>
                  <div className="rel-grid">
                    {meta.relations.map((r) => (
                      <label key={r}>
                        <input
                          type="checkbox"
                          checked={relations.includes(r)}
                          onChange={() => setRelations(relations.includes(r) ? relations.filter((x) => x !== r) : [...relations, r])}
                        />
                        <i className="dot" style={{ background: REL_COLORS[r] }} />
                        {r.replace("_", " ")}
                      </label>
                    ))}
                  </div>
                </div>
              )}
              {filterTab === "leaf" && (
                <div>
                  <p style={{ color: "var(--muted)", fontSize: 13 }}>
                    Matching words in the filtered language stay visible even when they have further descendants.
                  </p>
                  <PredicateEditor meta={meta} value={leaf} onChange={setLeaf} showPopularity />
                </div>
              )}
              {filterTab === "path" && (
                <div>
                  <p style={{ color: "var(--muted)", fontSize: 13 }}>
                    Constraints on intermediate hops between the etymon and each leaf.
                  </p>
                  <label className="field">Quantifier</label>
                  <select value={pathMatch} onChange={(e) => setPathMatch(e.target.value as Quantifier)}>
                    <option value="any">any hop matches</option>
                    <option value="all">every hop matches</option>
                    <option value="none">no hop matches</option>
                    <option value="exactly">exactly one hop matches</option>
                  </select>
                  <PredicateEditor
                    meta={meta}
                    value={pathNode}
                    onChange={setPathNode}
                    extra={
                      <>
                        <label className="field">Path must include relation</label>
                        <div className="rel-grid">
                          {meta.relations.map((r) => (
                            <label key={r}>
                              <input
                                type="checkbox"
                                checked={pathRelAny.includes(r)}
                                onChange={() =>
                                  setPathRelAny(pathRelAny.includes(r) ? pathRelAny.filter((x) => x !== r) : [...pathRelAny, r])
                                }
                              />
                              {r}
                            </label>
                          ))}
                        </div>
                        <label className="field">Path must exclude relation</label>
                        <div className="rel-grid">
                          {meta.relations.map((r) => (
                            <label key={`n${r}`}>
                              <input
                                type="checkbox"
                                checked={pathRelNone.includes(r)}
                                onChange={() =>
                                  setPathRelNone(pathRelNone.includes(r) ? pathRelNone.filter((x) => x !== r) : [...pathRelNone, r])
                                }
                              />
                              {r}
                            </label>
                          ))}
                        </div>
                      </>
                    }
                  />
                </div>
              )}
            </>
          )}
          </div>
        </aside>
        <main className="stage">
          {!term && meta && (
            <div className="examples">
              {meta.examples.map((ex) => (
                <button key={`${ex.term}-${ex.lang}`} onClick={() => pick(ex)}>
                  <span className="t">{ex.term}</span>
                  <span>{ex.lang_display || ex.lang}</span>
                  <span className="b">{ex.blurb || `${ex.child_count} derived`}</span>
                </button>
              ))}
            </div>
          )}
          {(view === "tree" || view === "radial") && <canvas ref={canvasRef} className="viz-canvas" />}
          {view === "map" && <div className="map" ref={mapRef} />}
          {view === "table" && (
            <div className="table-wrap">
              <table>
                <thead>
                  <tr>
                    <th>Term</th>
                    <th>Language</th>
                    <th>Family</th>
                    <th>Relation</th>
                    <th>Conf</th>
                    <th>Depth</th>
                  </tr>
                </thead>
                <tbody>
                  {words.map((n) => (
                    <tr
                      key={String(n.id)}
                      onClick={() => {
                        setSelected(n);
                        setMobilePanel("info");
                      }}
                    >
                      <td>{n.term}</td>
                      <td>{n.lang_display || n.lang}</td>
                      <td>{n.family_name}</td>
                      <td>{n.relation}</td>
                      <td>{n.confidence?.toFixed(2)}</td>
                      <td>{n.depth}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          )}
          {view === "stats" && tree && (
            <div className="stats">
              <h3>Relations</h3>
              <Bars data={tree.stats.relations || {}} />
              <h3>Families</h3>
              <Bars data={tree.stats.families || {}} />
              <h3>Depth</h3>
              <Bars data={tree.stats.depths || {}} />
            </div>
          )}
          <div className="legend">
            {Object.entries(REL_COLORS).map(([k, c]) => (
              <span key={k}>
                <i className="dot" style={{ background: c }} /> {k.replace("_", " ")}
              </span>
            ))}
          </div>
          <div className="status-line">
            {loading ? "querying…" : error ? error : tree ? `${tree.stats.kept_leaves} leaves · ${tree.stats.payload} shown · ${tree.stats.elapsed_ms} ms` : "Pick a word"}
            {tree?.truncated ? " · truncated" : ""}
          </div>
        </main>
        <aside className={`panel right inspector ${mobilePanel === "info" ? "open-sheet" : ""}`}>
          <div className="sheet-grab">
            <span />
            <strong>Inspector</strong>
            <button type="button" className="ghost sheet-close" onClick={() => setMobilePanel("none")}>
              Close
            </button>
          </div>
          <div className="sheet-body">
          {selected ? (
            <>
              <h2>{selected.term}</h2>
              <div>
                {selected.lang_display || selected.lang}
                {selected.family_name ? ` · ${selected.family_name}` : ""}
              </div>
              <div className="inspector-def">
                <h3>Definition</h3>
                {definitionLoading ? (
                  <p className="modal-muted">Looking up Wiktionary…</p>
                ) : definition?.senses?.length ? (
                  <>
                    {definition.fallback ? (
                      <p className="modal-muted">No exact language match — showing [{definition.matched_code}].</p>
                    ) : null}
                    <div className="modal-senses">
                      {definition.senses.map((s) => (
                        <div key={s.pos} className="modal-sense">
                          <div className="modal-pos">{s.pos}</div>
                          <ol>
                            {s.glosses.map((g) => (
                              <li key={g}>{g}</li>
                            ))}
                          </ol>
                        </div>
                      ))}
                    </div>
                  </>
                ) : (
                  <p className="modal-muted">
                    No Wiktionary gloss found
                    {definition?.error ? ` (${definition.error})` : ""}.
                  </p>
                )}
              </div>
              <dl className="kv">
                <dt>Relation</dt>
                <dd>{selected.relation || "root"}</dd>
                <dt>Confidence</dt>
                <dd>{selected.confidence ?? "—"}</dd>
                <dt>Depth</dt>
                <dd>{selected.depth}</dd>
                <dt>Children</dt>
                <dd>{selected.child_count}</dd>
                <dt>Area</dt>
                <dd>{selected.macroarea || "—"}</dd>
                <dt>Glottocode</dt>
                <dd>{selected.glottocode || "—"}</dd>
                <dt>Zipf</dt>
                <dd>{selected.zipf != null ? selected.zipf : "—"}</dd>
                <dt>Freq rank</dt>
                <dd>{selected.freq_rank != null ? selected.freq_rank.toLocaleString() : "—"}</dd>
              </dl>
              {selected.kind === "word" && (
                <div className="row">
                  <button className="ghost primary" onClick={() => pick({ term: selected.term, lang: selected.lang, child_count: selected.child_count || 0 })}>
                    Descend from here
                  </button>
                  <button className="ghost" onClick={() => setLeaf({ ...leaf, languages: [selected.lang] })}>
                    Leaf = this language
                  </button>
                </div>
              )}
              {detail && (
                <>
                  <h3>Comes from</h3>
                  <div className="path-list">
                    {((detail.etymons as SuggestHit[]) || []).map((e, i) => (
                      <button key={i} onClick={() => pick({ term: e.term, lang: e.lang, child_count: 0 })}>
                        {e.term} · {e.lang} · {(e as unknown as { relation?: string }).relation}
                      </button>
                    ))}
                  </div>
                  {!!(detail.wiktionary as string) && (
                    <p>
                      <a href={detail.wiktionary as string} target="_blank" rel="noreferrer" style={{ color: "var(--teal)" }}>
                        Wiktionary
                      </a>
                    </p>
                  )}
                </>
              )}
            </>
          ) : (
            <p style={{ color: "var(--muted)" }}>
              Select a node to inspect etymons, typology, and phoneme inventory. On a phone, filters and this inspector become bottom sheets.
            </p>
          )}
          {qr && (
            <div className="qr">
              <h3>Open this view on a phone</h3>
              <img src={qr} alt="QR code of current URL" />
            </div>
          )}
          {meta?.citation && (
            <p style={{ color: "var(--muted)", fontSize: 12, marginTop: 24 }}>
              Data: {meta.citation.author}, Etymology Atlas ({meta.citation.license}). {meta.nodes.toLocaleString()} words, {meta.edges.toLocaleString()} relations.
            </p>
          )}
          </div>
        </aside>
      </div>
    </div>
  );
}

function Bars({ data }: { data: Record<string, number> }) {
  const max = Math.max(1, ...Object.values(data));
  return (
    <div className="bars">
      {Object.entries(data).map(([k, v]) => (
        <div className="bar-row" key={k}>
          <span>{k}</span>
          <div className="bar">
            <i style={{ width: `${(100 * v) / max}%` }} />
          </div>
          <span>{v}</span>
        </div>
      ))}
    </div>
  );
}
