// palette.jsx — ⌘K command palette with smart tokens, grouped results, keyboard nav

// ── Scopes ───────────────────────────────────────────────────────
const PALETTE_SCOPES = [
  { id: "all",      label: "All",      icon: "Search" },
  { id: "tasks",    label: "Tasks",    icon: "Check" },
  { id: "epics",    label: "Epics",    icon: "Folder" },
  { id: "people",   label: "People",   icon: "Users" },
  { id: "projects", label: "Projects", icon: "Board" },
  { id: "sprints",  label: "Sprints",  icon: "Lightning" },
  { id: "actions",  label: "Actions",  icon: "Lightning" },
];

// ── Smart-token parser ──────────────────────────────────────────
// Recognizes:  @alex  is:high  status:doing  in:checkout  sprint:24  #guest  "exact phrase"
const TOKEN_RE = /(\w+):([^\s"]+|"[^"]*")|@(\w+)|#(\w+)|"([^"]+)"/g;

function parseQuery(raw) {
  const tokens = { people: [], statuses: [], priorities: [], projects: [], epics: [], sprints: [], phrases: [] };
  let text = raw;
  const matched = [];
  let m;
  while ((m = TOKEN_RE.exec(raw)) !== null) {
    matched.push([m.index, m.index + m[0].length]);
    if (m[3]) {
      tokens.people.push(m[3].toLowerCase());
    } else if (m[4]) {
      tokens.epics.push(m[4].toLowerCase());
    } else if (m[5]) {
      tokens.phrases.push(m[5].toLowerCase());
    } else if (m[1]) {
      const k = m[1].toLowerCase();
      const v = (m[2] || "").replace(/^"|"$/g, "").toLowerCase();
      if (k === "is" || k === "priority" || k === "prio") tokens.priorities.push(v);
      else if (k === "status") tokens.statuses.push(v);
      else if (k === "in" || k === "project") tokens.projects.push(v);
      else if (k === "epic") tokens.epics.push(v);
      else if (k === "sprint") tokens.sprints.push(v);
      else if (k === "by" || k === "owner" || k === "@") tokens.people.push(v);
    }
  }
  // Strip matched tokens from free text
  if (matched.length) {
    const parts = [];
    let cursor = 0;
    for (const [s, e] of matched) {
      parts.push(raw.slice(cursor, s));
      cursor = e;
    }
    parts.push(raw.slice(cursor));
    text = parts.join(" ").replace(/\s+/g, " ").trim();
  }
  tokens.text = text;
  tokens.textLower = text.toLowerCase();
  return tokens;
}

// ── Fuzzy score ──────────────────────────────────────────────────
function fuzzyScore(needle, haystack) {
  if (!needle) return 0.5;
  const n = needle.toLowerCase();
  const h = haystack.toLowerCase();
  if (h === n) return 1000;
  if (h.startsWith(n)) return 500 - (h.length - n.length) * 0.1;
  const idx = h.indexOf(n);
  if (idx !== -1) return 200 - idx;
  // Subsequence match
  let ni = 0, score = 0, prev = -1;
  for (let i = 0; i < h.length && ni < n.length; i++) {
    if (h[i] === n[ni]) {
      score += (prev === i - 1 ? 8 : 3);
      prev = i;
      ni++;
    }
  }
  return ni === n.length ? score : -1;
}

// ── Highlighter ──────────────────────────────────────────────────
function highlight(text, query) {
  if (!query) return text;
  const lo = text.toLowerCase();
  const q = query.toLowerCase();
  const i = lo.indexOf(q);
  if (i === -1) return text;
  return (
    <>
      {text.slice(0, i)}
      <mark>{text.slice(i, i + query.length)}</mark>
      {text.slice(i + query.length)}
    </>
  );
}

// ── Recent items (localStorage) ──────────────────────────────────
const RECENT_KEY = "flowboard.palette.recent";
function loadRecent() {
  try { return JSON.parse(localStorage.getItem(RECENT_KEY) || "[]"); } catch { return []; }
}
function pushRecent(item) {
  try {
    const cur = loadRecent().filter(r => r.key !== item.key);
    cur.unshift({ ...item, at: Date.now() });
    localStorage.setItem(RECENT_KEY, JSON.stringify(cur.slice(0, 6)));
  } catch {}
}

// ── Build the index ──────────────────────────────────────────────
function buildIndex() {
  const items = [];

  // Tasks
  for (const t of ALL_TASKS) {
    items.push({
      key: `task:${t.id}`, kind: "task", id: t.id,
      title: t.name,
      meta: [t.epicTitle || "Quick tasks", t.sprint ? (SPRINTS.find(s => s.id === t.sprint)?.label || t.sprint) : null].filter(Boolean),
      task: t,
      status: t.status, prio: t.prio, owners: t.owners || [], sprint: t.sprint,
      epicId: t.epicId, projectId: "checkout",
      search: [t.name, t.epicTitle, ...(t.owners||[]).map(o => PEOPLE.find(p=>p.id===o)?.name || "")].join(" "),
    });
  }
  // MY_TASKS extra projects
  for (const t of MY_TASKS) {
    if (items.find(i => i.kind === "task" && i.title === t.name)) continue;
    items.push({
      key: `task:mt-${t.id}`, kind: "task", id: t.id,
      title: t.name,
      meta: [t.project, t.epic].filter(Boolean),
      task: t, status: t.status, prio: t.prio,
      search: [t.name, t.project, t.epic].join(" "),
    });
  }

  // Epics
  for (const e of EPICS) {
    items.push({
      key: `epic:${e.id}`, kind: "epic", id: e.id,
      title: e.title, color: e.color,
      meta: [`${e.tasks.length} tasks`, "Checkout v2"],
      search: e.title + " epic",
    });
  }
  // People
  for (const p of PEOPLE) {
    items.push({
      key: `person:${p.id}`, kind: "person", id: p.id,
      title: p.name, person: p,
      meta: [`@${p.id}`, "Engineer"],
      search: p.name + " " + p.id,
    });
  }
  // Projects
  for (const p of PROJECTS) {
    items.push({
      key: `project:${p.id}`, kind: "project", id: p.id,
      title: p.name, color: p.color,
      meta: [`${p.count} tasks`, "Team Tabsyst"],
      search: p.name,
    });
  }
  // Sprints
  for (const s of SPRINTS) {
    items.push({
      key: `sprint:${s.id}`, kind: "sprint", id: s.id,
      title: s.label,
      meta: [s.team, s.dates, s.active ? "Active" : "Closed"].filter(Boolean),
      search: s.label + " " + s.team,
    });
  }
  // Static actions
  const actions = [
    { id: "new-task",   title: "Create new task",           meta: ["Press N"], icon: "Plus" },
    { id: "new-sprint", title: "Start new sprint",          meta: ["Sprint planning"], icon: "Lightning" },
    { id: "go-sprint",  title: "Go to Sprint board",        meta: ["Navigate"], icon: "Lightning" },
    { id: "go-backlog", title: "Go to Backlog",             meta: ["Navigate"], icon: "Inbox" },
    { id: "go-kanban",  title: "Go to Kanban",              meta: ["Navigate"], icon: "Board" },
    { id: "go-mywork",  title: "Go to My Work",             meta: ["Navigate"], icon: "Home" },
    { id: "invite",     title: "Invite teammate",           meta: ["Collaboration"], icon: "Users" },
  ];
  for (const a of actions) {
    items.push({
      key: `action:${a.id}`, kind: "action", id: a.id,
      title: a.title, meta: a.meta, icon: a.icon,
      search: a.title + " " + a.meta.join(" "),
    });
  }
  return items;
}

// ── Filter + score items against parsed query ──────────────────
function filterItems(items, parsed, scope) {
  const { text, textLower, people, statuses, priorities, projects, epics, sprints, phrases } = parsed;

  const hasTokens = people.length || statuses.length || priorities.length || projects.length || epics.length || sprints.length || phrases.length;

  return items
    .filter(it => {
      if (scope !== "all") {
        if (scope === "tasks" && it.kind !== "task") return false;
        if (scope === "epics" && it.kind !== "epic") return false;
        if (scope === "people" && it.kind !== "person") return false;
        if (scope === "projects" && it.kind !== "project") return false;
        if (scope === "sprints" && it.kind !== "sprint") return false;
        if (scope === "actions" && it.kind !== "action") return false;
      }
      // Token filters apply mostly to tasks
      if (people.length && it.kind === "task") {
        const ownerNames = (it.owners || []).flatMap(o => {
          const p = PEOPLE.find(x => x.id === o);
          return p ? [p.id, p.name.toLowerCase(), p.name.split(" ")[0].toLowerCase()] : [o];
        });
        if (!people.some(pp => ownerNames.some(on => on.includes(pp)))) return false;
      }
      if (statuses.length && it.kind === "task") {
        if (!statuses.some(s => (it.status || "").includes(s) || s.includes(it.status || ""))) return false;
      }
      if (priorities.length && it.kind === "task") {
        if (!priorities.some(p => (it.prio || "").includes(p) || p.includes(it.prio || ""))) return false;
      }
      if (epics.length && it.kind === "task") {
        if (!epics.some(e => (it.epicId || "").includes(e) || (it.task?.epicTitle || "").toLowerCase().includes(e))) return false;
      }
      if (sprints.length && it.kind === "task") {
        if (!sprints.some(s => (it.sprint || "").includes(s))) return false;
      }
      if (phrases.length) {
        const hay = (it.search || it.title).toLowerCase();
        if (!phrases.every(p => hay.includes(p))) return false;
      }
      return true;
    })
    .map(it => {
      const s = fuzzyScore(textLower, it.search || it.title);
      return { ...it, score: s };
    })
    .filter(it => it.score > -1 || !text)
    .sort((a, b) => {
      // Kind priority boost for empty query
      const kindRank = (k) => ({ action: 0, task: 1, epic: 2, person: 3, project: 4, sprint: 5 }[k] ?? 6);
      if (!text && !hasTokens) return kindRank(a.kind) - kindRank(b.kind);
      return b.score - a.score;
    });
}

// ── Row renderers ────────────────────────────────────────────────
function PaletteTaskRow({ item, query }) {
  const status = STATUSES.find(s => s.id === item.status);
  const prio = PRIORITIES.find(p => p.id === item.prio);
  const owners = (item.owners || []).slice(0, 2);
  return (
    <>
      <div className="palette-row-icon"
           style={{ background: item.task?.epicColor || item.color || "var(--surface-2)", color: "#fff" }}>
        <Icons.Check size={12}/>
      </div>
      <div className="palette-row-main">
        <div className="palette-row-title">{highlight(item.title, query)}</div>
        <div className="palette-row-meta">
          {status && <span className={`pill ${status.cls}`} style={{ fontSize: 10, padding: "1px 6px" }}>{status.label}</span>}
          {prio && <><span className="palette-row-meta-sep">·</span><span>{prio.label}</span></>}
          {item.meta.map((m, i) => <React.Fragment key={i}><span className="palette-row-meta-sep">·</span><span>{m}</span></React.Fragment>)}
        </div>
      </div>
      <div className="palette-row-right">
        {owners.length > 0 && <AvatarStack ids={owners} max={2}/>}
        <span className="palette-enter">Open <Icons.ArrowUp size={10} style={{ transform: "rotate(90deg)" }}/></span>
      </div>
    </>
  );
}

function PaletteGenericRow({ item, query, iconName, tag }) {
  const Icon = Icons[iconName] || Icons.Search;
  return (
    <>
      {item.kind === "epic" ? (
        <span className="palette-row-icon--epic" style={{ background: item.color }}/>
      ) : item.kind === "person" ? (
        <Avatar person={item.person} size="sm"/>
      ) : (
        <div className="palette-row-icon" style={item.color ? { background: item.color, color: "#fff" } : {}}>
          <Icon size={12}/>
        </div>
      )}
      <div className="palette-row-main">
        <div className="palette-row-title">{highlight(item.title, query)}</div>
        <div className="palette-row-meta">
          {tag && <><span className="palette-row-meta-sep" style={{ opacity: 1, textTransform: "uppercase", fontWeight: 700, fontSize: 10, letterSpacing: ".06em" }}>{tag}</span><span className="palette-row-meta-sep">·</span></>}
          {item.meta.map((m, i) => <React.Fragment key={i}>{i > 0 && <span className="palette-row-meta-sep">·</span>}<span>{m}</span></React.Fragment>)}
        </div>
      </div>
      <div className="palette-row-right">
        <span className="palette-enter">Jump <Icons.ArrowUp size={10} style={{ transform: "rotate(90deg)" }}/></span>
      </div>
    </>
  );
}

// ── Chips that show parsed tokens ───────────────────────────────
function PaletteTokenChips({ parsed, onRemove }) {
  const chips = [];
  for (const p of parsed.people) {
    const person = PEOPLE.find(x => x.id === p || x.name.toLowerCase().includes(p));
    chips.push({ key: `@${p}`, label: person?.name || `@${p}`, color: person?.color, kind: "person" });
  }
  for (const s of parsed.statuses) chips.push({ key: `status:${s}`, label: `status: ${s}`, kind: "status" });
  for (const pr of parsed.priorities) chips.push({ key: `is:${pr}`, label: `is: ${pr}`, kind: "prio" });
  for (const pr of parsed.projects) chips.push({ key: `in:${pr}`, label: `in: ${pr}`, kind: "project" });
  for (const e of parsed.epics) chips.push({ key: `#${e}`, label: `#${e}`, kind: "epic" });
  for (const s of parsed.sprints) chips.push({ key: `sprint:${s}`, label: `sprint: ${s}`, kind: "sprint" });
  for (const ph of parsed.phrases) chips.push({ key: `"${ph}"`, label: `"${ph}"`, kind: "phrase" });

  if (!chips.length) return null;
  return (
    <div className="palette-chips">
      {chips.map(c => (
        <span key={c.key} className="palette-chip">
          {c.color && <span className="palette-chip-dot" style={{ background: c.color }}/>}
          {c.label}
          <button className="palette-chip-x" onClick={(e) => { e.stopPropagation(); onRemove(c.key); }}>
            <Icons.Close size={11}/>
          </button>
        </span>
      ))}
    </div>
  );
}

// ── Genie-bar morph helpers ─────────────────────────────────────
// We treat the topbar search bar as the "lamp": when the palette opens,
// the card materializes from the bar's exact position/size and grows up
// to its centered pose. Closing reverses it. The transition is a CSS
// transition on transform/opacity/filter — JS only sets the "from" pose
// CSS variables once, then toggles `.is-open` on the card.
const GENIE_TRIGGER_SELECTOR = ".topbar-search.is-palette-trigger";

function readGenieFromPose(paletteEl) {
  const trigger = document.querySelector(GENIE_TRIGGER_SELECTOR);
  if (!trigger || !paletteEl) return null;
  const sb = trigger.getBoundingClientRect();
  const pr = paletteEl.getBoundingClientRect();
  if (pr.width === 0 || pr.height === 0) return null;
  const palCx = pr.left + pr.width / 2;
  const palCy = pr.top + pr.height / 2;
  const sbCx  = sb.left + sb.width / 2;
  const sbCy  = sb.top + sb.height / 2;
  return {
    tx: sbCx - palCx,
    ty: sbCy - palCy,
    sx: Math.max(0.18, sb.width / pr.width),
    sy: Math.max(0.04, sb.height / pr.height),
    trigger,
  };
}

function applyGeniePose(paletteEl, pose) {
  if (!paletteEl || !pose) return;
  paletteEl.style.setProperty("--genie-tx", pose.tx + "px");
  paletteEl.style.setProperty("--genie-ty", pose.ty + "px");
  paletteEl.style.setProperty("--genie-sx", String(pose.sx));
  paletteEl.style.setProperty("--genie-sy", String(pose.sy));
  paletteEl.style.setProperty("--genie-blur", "4px");
  paletteEl.style.setProperty("--genie-radius", "18px");
}

// ── Main palette ─────────────────────────────────────────────────
function CommandPalette({ open, onClose, onNavigate }) {
  const [raw, setRaw] = React.useState("");
  const [scope, setScope] = React.useState("all");
  const [activeIdx, setActiveIdx] = React.useState(0);
  const inputRef = React.useRef(null);
  const resultsRef = React.useRef(null);
  const paletteRef = React.useRef(null);

  // Animation phase machine:
  //   closed  → unmounted
  //   opening → mounted at "from" pose (search-bar shape), waiting for next frame
  //   open    → at identity (full card), is-open class on
  //   closing → animating back toward "from" pose, will unmount when done
  const [phase, setPhase] = React.useState("closed");

  const index = React.useMemo(() => buildIndex(), []);
  const parsed = React.useMemo(() => parseQuery(raw), [raw]);
  const results = React.useMemo(() => filterItems(index, parsed, scope), [index, parsed, scope]);

  const recent = React.useMemo(() => {
    if (raw.trim()) return [];
    const r = loadRecent();
    return r.map(rec => index.find(i => i.key === rec.key)).filter(Boolean);
  }, [raw, open, index]);

  // Group results
  const groups = React.useMemo(() => {
    const g = { task: [], epic: [], person: [], project: [], sprint: [], action: [] };
    for (const r of results) g[r.kind]?.push(r);
    // Cap each group for the "all" scope so it feels balanced
    if (scope === "all") {
      g.task = g.task.slice(0, 6);
      g.epic = g.epic.slice(0, 3);
      g.person = g.person.slice(0, 4);
      g.project = g.project.slice(0, 3);
      g.sprint = g.sprint.slice(0, 3);
      g.action = g.action.slice(0, 4);
    }
    return g;
  }, [results, scope]);

  // Flat list in render order for keyboard nav
  const flat = React.useMemo(() => {
    const order = scope === "all"
      ? ["action", "task", "epic", "person", "project", "sprint"]
      : scope === "actions" ? ["action"] : [scope.replace(/s$/, "") === "people" ? "person" : scope.slice(0, -1)];
    const out = [];
    for (const k of order) (groups[k] || []).forEach(it => out.push(it));
    return out;
  }, [groups, scope]);

  React.useEffect(() => { setActiveIdx(0); }, [raw, scope]);

  // ── Phase machine: drive open/close based on the `open` prop ──
  React.useEffect(() => {
    if (open) {
      // If we're closed (or about to be), kick off opening.
      if (phase === "closed" || phase === "closing") {
        setPhase("opening");
      }
    } else {
      // Trigger close animation only if we're currently visible.
      if (phase === "open" || phase === "opening") {
        setPhase("closing");
      }
    }
  }, [open]); // eslint-disable-line react-hooks/exhaustive-deps

  // ── Apply genie "from" pose, then transition to "open" ──
  React.useEffect(() => {
    if (phase === "opening" && paletteRef.current) {
      const pose = readGenieFromPose(paletteRef.current);
      applyGeniePose(paletteRef.current, pose);
      // Hide the topbar trigger so the card looks like it absorbed the bar.
      pose?.trigger?.classList.add("is-genie-launching");
      // Two RAFs so the browser registers the "from" pose before we flip.
      let raf2 = 0;
      const raf1 = requestAnimationFrame(() => {
        raf2 = requestAnimationFrame(() => setPhase("open"));
      });
      // Focus the input shortly after — give CSS a beat to start the morph.
      const focusT = setTimeout(() => inputRef.current?.focus(), 80);
      return () => { cancelAnimationFrame(raf1); cancelAnimationFrame(raf2); clearTimeout(focusT); };
    }
    if (phase === "closing" && paletteRef.current) {
      // Re-measure in case the layout changed (window resize while open).
      const pose = readGenieFromPose(paletteRef.current);
      applyGeniePose(paletteRef.current, pose);
      const t = setTimeout(() => {
        // Restore the topbar trigger and fully unmount.
        const trigger = document.querySelector(GENIE_TRIGGER_SELECTOR);
        trigger?.classList.remove("is-genie-launching");
        setPhase("closed");
      }, 280);
      return () => clearTimeout(t);
    }
    if (phase === "closed") {
      // Fresh state for next open.
      setRaw(""); setScope("all"); setActiveIdx(0);
      const trigger = document.querySelector(GENIE_TRIGGER_SELECTOR);
      trigger?.classList.remove("is-genie-launching");
    }
  }, [phase]);

  // Scope counts
  const scopeCounts = React.useMemo(() => {
    const all = filterItems(index, parsed, "all");
    return {
      all: all.length,
      tasks: all.filter(i => i.kind === "task").length,
      epics: all.filter(i => i.kind === "epic").length,
      people: all.filter(i => i.kind === "person").length,
      projects: all.filter(i => i.kind === "project").length,
      sprints: all.filter(i => i.kind === "sprint").length,
      actions: all.filter(i => i.kind === "action").length,
    };
  }, [index, parsed]);

  // Scroll active row into view (within scroll container only)
  React.useEffect(() => {
    const el = resultsRef.current?.querySelector(".palette-row.is-active");
    if (el && resultsRef.current) {
      const rect = el.getBoundingClientRect();
      const pr = resultsRef.current.getBoundingClientRect();
      if (rect.top < pr.top) resultsRef.current.scrollTop -= (pr.top - rect.top) + 8;
      else if (rect.bottom > pr.bottom) resultsRef.current.scrollTop += (rect.bottom - pr.bottom) + 8;
    }
  }, [activeIdx]);

  function choose(item) {
    if (!item) return;
    pushRecent({ key: item.key, kind: item.kind, title: item.title });
    onNavigate && onNavigate(item);
    onClose();
  }

  function handleKey(e) {
    if (e.key === "Escape") { e.preventDefault(); onClose(); return; }
    if (e.key === "ArrowDown") { e.preventDefault(); setActiveIdx(i => Math.min(flat.length - 1, i + 1)); return; }
    if (e.key === "ArrowUp")   { e.preventDefault(); setActiveIdx(i => Math.max(0, i - 1)); return; }
    if (e.key === "Enter")     { e.preventDefault(); choose(flat[activeIdx]); return; }
    if (e.key === "Tab") {
      e.preventDefault();
      const ids = PALETTE_SCOPES.map(s => s.id);
      const cur = ids.indexOf(scope);
      const dir = e.shiftKey ? -1 : 1;
      setScope(ids[(cur + dir + ids.length) % ids.length]);
      return;
    }
    // Backspace on empty text removes last chip
    if (e.key === "Backspace" && raw === "" && parsed) {
      // no-op — there are no chips when raw is empty
    }
  }

  function removeChip(chipKey) {
    const re = new RegExp(chipKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\s?", "g");
    setRaw(raw.replace(re, "").trim());
  }

  // Global hotkey
  React.useEffect(() => {
    function onKey(e) {
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
        e.preventDefault();
        if (open) onClose(); else window.dispatchEvent(new CustomEvent("palette:open"));
      }
    }
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, [open, onClose]);

  // Stay mounted during the closing animation; only unmount when fully closed.
  if (phase === "closed") return null;

  const groupConfig = [
    { key: "action",  label: "Quick actions", iconName: "Lightning", tag: null },
    { key: "task",    label: "Tasks",         iconName: "Check",     tag: null },
    { key: "epic",    label: "Epics",         iconName: "Folder",    tag: "epic" },
    { key: "person",  label: "People",        iconName: "Users",     tag: null },
    { key: "project", label: "Projects",      iconName: "Board",     tag: null },
    { key: "sprint",  label: "Sprints",       iconName: "Lightning", tag: null },
  ];

  const showEmpty = flat.length === 0 && !recent.length;
  const showRecent = recent.length > 0 && !raw.trim() && scope === "all";

  return (
    <div className={`palette-backdrop ${phase === "open" ? "is-open" : ""} ${phase === "closing" ? "is-closing" : ""}`}
         onClick={onClose}>
      <div className={`palette ${phase === "open" ? "is-open" : ""}`}
           ref={paletteRef}
           onClick={(e) => e.stopPropagation()}>
        <div className="palette-searchbar">
          <Icons.Search size={16}/>
          <PaletteTokenChips parsed={parsed} onRemove={removeChip}/>
          <input
            ref={inputRef}
            className="palette-input"
            value={raw}
            onChange={(e) => setRaw(e.target.value)}
            onKeyDown={handleKey}
            placeholder={parsed.text || raw ? "" : "Search tasks, jump to people, run actions…   try @alex  is:high  #guest"}
          />
          <span className="palette-kbd palette-esc">ESC</span>
        </div>

        <div className="palette-scope">
          {PALETTE_SCOPES.map(s => {
            const Icon = Icons[s.icon] || Icons.Search;
            const count = scopeCounts[s.id];
            return (
              <button key={s.id}
                      className={scope === s.id ? "is-active" : ""}
                      onClick={() => setScope(s.id)}>
                <Icon size={12}/>
                {s.label}
                {count > 0 && <span className="palette-scope-count">{count}</span>}
              </button>
            );
          })}
        </div>

        <div className="palette-results" ref={resultsRef}>
          {showEmpty ? (
            <div className="palette-empty">
              <div className="palette-empty-title">No matches</div>
              <div>Try a different search term or scope.</div>
              <div className="palette-empty-hint">
                Tip: filter with <code>@name</code>, <code>is:high</code>, <code>status:doing</code>, <code>#epic</code>
              </div>
            </div>
          ) : (
            <>
              {showRecent && (
                <>
                  <div className="palette-group-label">Recent</div>
                  {recent.map((item, i) => (
                    <PaletteRow key={`r-${item.key}`} item={item} active={false}
                                onHover={() => {}} onClick={() => choose(item)} query={parsed.text}/>
                  ))}
                </>
              )}
              {groupConfig.map(g => {
                const items = groups[g.key] || [];
                if (!items.length) return null;
                return (
                  <React.Fragment key={g.key}>
                    <div className="palette-group-label">
                      {g.label} <span style={{ fontWeight: 500, color: "var(--ink-muted)", letterSpacing: 0, textTransform: "none" }}>{items.length}</span>
                    </div>
                    {items.map((item) => {
                      const i = flat.indexOf(item);
                      return (
                        <PaletteRow key={item.key}
                                    item={item}
                                    active={i === activeIdx}
                                    onHover={() => setActiveIdx(i)}
                                    onClick={() => choose(item)}
                                    query={parsed.text}
                                    iconName={g.iconName}
                                    tag={g.tag}/>
                      );
                    })}
                  </React.Fragment>
                );
              })}
            </>
          )}
        </div>

        <div className="palette-footer">
          <span className="palette-footer-item"><span className="palette-kbd">↑</span><span className="palette-kbd">↓</span> Navigate</span>
          <span className="palette-footer-item"><span className="palette-kbd">⏎</span> Open</span>
          <span className="palette-footer-item"><span className="palette-kbd">Tab</span> Switch scope</span>
          <span className="palette-footer-spacer"/>
          <span className="palette-footer-brand">ZeroProject search</span>
        </div>
      </div>
    </div>
  );
}

function PaletteRow({ item, active, onHover, onClick, query, iconName, tag }) {
  return (
    <div className={`palette-row ${active ? "is-active" : ""}`}
         onMouseEnter={onHover}
         onClick={onClick}>
      {item.kind === "task"
        ? <PaletteTaskRow item={item} query={query}/>
        : <PaletteGenericRow item={item} query={query} iconName={iconName || {epic: "Folder", person: "Users", project: "Board", sprint: "Lightning", action: "Lightning"}[item.kind]} tag={tag}/>}
    </div>
  );
}

Object.assign(window, { CommandPalette });
