// shell.jsx - app shell: sidebar nav, topbar, route container

// Sidebar IA: grouped sections. Renderer prints each section's label, then
// its items. Items inside `children` of a `group:true` node still render as
// the existing expandable submenu (only used for the Capture-SDK admin tools
// today, but kept general).
const NAV_SECTIONS = [
  {
    label: "Workspace",
    items: [
      { id: "home", label: "Home", icon: "home" },
      { id: "dashboard", label: "Dashboard", icon: "layout" },
      { id: "projects", label: "Projects", icon: "folder" },
      { id: "library", label: "Library", icon: "library" },
    ],
  },
  {
    label: "Ingest",
    items: [
      { id: "upload", label: "Upload", icon: "upload" },
      { id: "youtube", label: "YouTube", icon: "import" },
      { id: "recorders", label: "Recorders", icon: "record" },
      { id: "schedule", label: "Schedule", icon: "clock" },
      { id: "monitors", label: "Monitors", icon: "monitor" },
    ],
  },
  {
    label: "Operations",
    items: [
      { id: "playout", label: "Playout", icon: "signal" },
      { id: "jobs", label: "Jobs", icon: "jobs" },
    ],
  },
  {
    label: "Admin",
    items: [
      { id: "users", label: "Users", icon: "users" },
      { id: "tokens", label: "Tokens", icon: "token" },
      { id: "billing", label: "Billing", icon: "dollar" },
      { id: "containers", label: "Containers", icon: "container" },
      { id: "cluster", label: "Cluster", icon: "cluster" },
      { id: "settings", label: "Settings", icon: "settings" },
    ],
  },
];

// No hidden routes currently; Billing (the satirical pricing page) lives in
// the Admin section above. Real API token management is at /tokens.
const NAV_HIDDEN = [];

// Back-compat: NAV_TREE and ADMIN_TREE were used by other modules.
// NAV_FLAT is consumed by topbar search and the keyboard router.
const NAV_TREE = NAV_SECTIONS.slice(0, 3).flatMap(s => s.items);
const ADMIN_TREE = NAV_SECTIONS[3].items;
const NAV_FLAT = (() => {
  const out = [];
  NAV_SECTIONS.forEach(s => s.items.forEach(n => out.push({ id: n.id, label: n.label, icon: n.icon })));
  NAV_HIDDEN.forEach(n => out.push({ id: n.id, label: n.label, icon: n.icon }));
  return out;
})();

function NavItem({ item, active, onSelect, depth = 0, openGroups, toggleGroup }) {
  const isGroup = item.group && item.children;
  const isOpen = isGroup && openGroups.has(item.id);
  const isActive = active === item.id || (isGroup && item.children.some(c => c.id === active));

  return (
    <>
      <div
        className={`nav-item ${isActive && !isGroup ? "active" : ""} ${isGroup ? "has-children" : ""} ${isOpen ? "open" : ""}`}
        data-tip={item.label}
        onClick={() => {
          if (isGroup) toggleGroup(item.id);
          else onSelect(item.id);
        }}
      >
        <Icon name={item.icon} size={15} className="nav-icon" />
        <span>{item.label}</span>
        {item.badge && (
          <span className={`nav-badge ${item.badge.kind}`}>{item.badge.text}</span>
        )}
        {isGroup && <Icon name="chevron" size={12} className="nav-caret" />}
      </div>
      {isGroup && isOpen && (
        <div className="nav-children">
          {item.children.map(c => (
            <div
              key={c.id}
              className={`nav-item ${active === c.id ? "active" : ""}`}
              onClick={() => onSelect(c.id)}
            >
              <Icon name={c.icon} size={14} className="nav-icon" />
              <span>{c.label}</span>
              {c.badge && <span className={`nav-badge ${c.badge.kind}`}>{c.badge.text}</span>}
            </div>
          ))}
        </div>
      )}
    </>
  );
}

function Sidebar({ active, onNavigate, me, collapsed, onToggle }) {
  const [openGroups, setOpenGroups] = React.useState(new Set([]));
  const [jobsBadge, setJobsBadge] = React.useState(null);

  // Live jobs count (#130): poll /jobs?status=active and render the result
  // as the sidebar badge on the Jobs item. Falls back to hidden on error.
  React.useEffect(() => {
    let cancelled = false;
    const tick = () => {
      window.ZAMPP_API.fetch('/jobs?status=active&limit=200')
        .then(d => {
          if (cancelled) return;
          const list = Array.isArray(d) ? d : (d?.jobs || d?.items || []);
          const n = Array.isArray(list) ? list.length : 0;
          setJobsBadge(n > 0 ? { kind: n > 5 ? 'warning' : 'neutral', text: n > 99 ? '99+' : String(n) } : null);
        })
        .catch(() => setJobsBadge(null));
    };
    tick();
    const id = setInterval(tick, 10000);
    return () => { cancelled = true; clearInterval(id); };
  }, []);

  // (Capture live-signal badge previously lived here; it now belongs in the
  // topbar status pip alongside the cluster pip. See issue #149.)

  // Apply the live Jobs badge to the Operations section, and gate the Admin
  // section to admins only (RBAC v2). Non-admins never see Users/Cluster/etc.
  // This is UX only — the API enforces the same rules server-side.
  const isAdmin = me?.role === 'admin';
  const sections = React.useMemo(
    () => NAV_SECTIONS
      .filter(sec => sec.label !== 'Admin' || isAdmin)
      .map(sec => ({
        ...sec,
        items: sec.items.map(n => (n.id === 'jobs' && jobsBadge) ? { ...n, badge: jobsBadge } : n),
      })),
    [jobsBadge, isAdmin]
  );
  const toggleGroup = (id) => {
    setOpenGroups(prev => {
      const next = new Set(prev);
      if (next.has(id)) next.delete(id);
      else next.add(id);
      return next;
    });
  };

  React.useEffect(() => {
    const ingestChildren = ["upload", "youtube", "recorders", "schedule", "monitors"];
    if (ingestChildren.includes(active)) {
      setOpenGroups(prev => prev.has("ingest") ? prev : new Set([...prev, "ingest"]));
    }
  }, [active]);

  return (
    <aside className="sidebar">
      <div className="sidebar-header">
        <div
          className="brand-link"
          onClick={() => onNavigate('home')}
          style={{ cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 10, flex: 1, minWidth: 0 }}
          title="Home"
        >
          <img className="brand-logo" src="img/dragon-logo.png" alt="Dragonflight" draggable="false" />
          <div className="brand-name">Dragonflight</div>
        </div>
        <button
          className="icon-btn sidebar-toggle"
          onClick={onToggle}
          title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
          aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
          aria-expanded={!collapsed}
        >
          <Icon name="chevron" size={14} style={{ transform: collapsed ? 'rotate(0deg)' : 'rotate(180deg)', transition: 'transform 120ms' }} />
        </button>
      </div>
      <div className="sidebar-scroll">
        {sections.map((section, si) => (
          <React.Fragment key={section.label}>
            {si > 0 && <div className="nav-section-label">{section.label}</div>}
            {section.items.map(item => (
              <NavItem
                key={item.id}
                item={item}
                active={active}
                onSelect={onNavigate}
                openGroups={openGroups}
                toggleGroup={toggleGroup}
              />
            ))}
          </React.Fragment>
        ))}
      </div>
      <div className="sidebar-footer">
        <div className="avatar">{me?.initials || '·'}</div>
        <div className="user-meta">
          <div className="user-name">{me?.name || 'Not signed in'}</div>
          <div className="user-role" title={me?.synthetic ? 'AUTH_ENABLED=false: showing the OS user running the server' : ''}>
            {me?.role || '·'}{me?.synthetic ? ' · auth off' : ''}
          </div>
        </div>
        {me?.synthetic ? null : (
          <button className="icon-btn" aria-label="Sign out" data-tip="Sign out" title="Sign out"
            onClick={async () => {
              try { await window.ZAMPP_API.fetch('/auth/logout', { method: 'POST' }); } catch (_) {}
              window.AuthGate.bounce('user signed out');
            }}>
            <Icon name="power" />
          </button>
        )}
      </div>
    </aside>
  );
}

function assetSearchIcon(a) {
  const t = (a.type || a.media_type || '').toLowerCase();
  if (t.includes('audio')) return 'audio';
  if (t.includes('image')) return 'image';
  return 'film';
}

function GlobalSearch({ onNavigate, onOpenAsset, onOpenProject }) {
  const [q, setQ] = React.useState('');
  const [open, setOpen] = React.useState(false);
  const [sel, setSel] = React.useState(0);
  const inputRef = React.useRef(null);
  const listRef = React.useRef(null);
  const isMac = typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.platform);

  React.useEffect(() => {
    const handler = (e) => {
      const key = e.key && e.key.toLowerCase();
      if ((e.metaKey || e.ctrlKey) && key === 'k') {
        e.preventDefault();
        if (inputRef.current) {
          inputRef.current.focus();
          inputRef.current.select();
          setOpen(true);
        }
      }
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, []);

  const results = React.useMemo(() => {
    const term = q.trim().toLowerCase();
    if (!term) return [];
    const D = window.ZAMPP_DATA || {};
    const out = [];

    (D.ASSETS || []).forEach(a => {
      const hay = ((a.name || '') + ' ' + (a.project || '') + ' ' + (a.filename || '')).toLowerCase();
      if (hay.includes(term)) {
        const sub = [a.project, a.res, a.duration].filter(x => x && x !== '·').join(' · ');
        out.push({ kind: 'asset', icon: assetSearchIcon(a), label: a.name, sub, item: a });
      }
    });
    (D.PROJECTS || []).forEach(p => {
      if ((p.name || '').toLowerCase().includes(term)) {
        out.push({ kind: 'project', icon: 'folder', label: p.name, sub: (p.assets || 0) + ' assets', item: p, color: p.color });
      }
    });
    (D.RECORDERS || []).forEach(r => {
      const hay = ((r.name || '') + ' ' + (r.url || '') + ' ' + (r.source || '')).toLowerCase();
      if (hay.includes(term)) {
        out.push({ kind: 'recorder', icon: 'record', label: r.name || r.url || 'Recorder', sub: [r.status, r.source].filter(Boolean).join(' · '), item: r });
      }
    });
    (D.JOBS || []).forEach(j => {
      const hay = ((j.asset || '') + ' ' + (j.kind || '') + ' ' + (j.status || '')).toLowerCase();
      if (hay.includes(term)) {
        out.push({ kind: 'job', icon: 'jobs', label: j.asset || j.kind || 'Job', sub: [j.kind, j.status].filter(Boolean).join(' · '), item: j });
      }
    });
    (D.USERS || []).forEach(u => {
      const hay = ((u.name || '') + ' ' + (u.email || '') + ' ' + (u.username || '')).toLowerCase();
      if (hay.includes(term)) {
        out.push({ kind: 'user', icon: 'users', label: u.name, sub: u.email || u.role, item: u });
      }
    });
    NAV_FLAT.forEach(n => {
      if (n.label.toLowerCase().includes(term)) {
        out.push({ kind: 'nav', icon: n.icon, label: n.label, sub: 'Go to ' + n.label, target: n.id });
      }
    });

    return out.slice(0, 40);
  }, [q, open]);

  React.useEffect(() => { setSel(0); }, [q]);

  React.useEffect(() => {
    if (!listRef.current) return;
    const el = listRef.current.querySelector('.search-result.active');
    if (el && el.scrollIntoView) el.scrollIntoView({ block: 'nearest' });
  }, [sel]);

  const handleSelect = (r) => {
    if (!r) return;
    setOpen(false);
    setQ('');
    if (inputRef.current) inputRef.current.blur();
    if (r.kind === 'asset' && onOpenAsset) onOpenAsset(r.item);
    else if (r.kind === 'project' && onOpenProject) onOpenProject(r.item);
    else if (r.kind === 'recorder') onNavigate && onNavigate('recorders');
    else if (r.kind === 'job') onNavigate && onNavigate('jobs');
    else if (r.kind === 'user') onNavigate && onNavigate('users');
    else if (r.kind === 'nav') onNavigate && onNavigate(r.target);
  };

  const onKeyDown = (e) => {
    if (e.key === 'Escape') {
      setOpen(false); setQ('');
      if (inputRef.current) inputRef.current.blur();
      return;
    }
    if (!open || results.length === 0) return;
    if (e.key === 'ArrowDown') { e.preventDefault(); setSel(s => Math.min(s + 1, results.length - 1)); }
    else if (e.key === 'ArrowUp') { e.preventDefault(); setSel(s => Math.max(0, s - 1)); }
    else if (e.key === 'Enter') { e.preventDefault(); handleSelect(results[sel]); }
  };

  const showDropdown = open && q.trim().length > 0;

  const listboxId = 'global-search-listbox';
  return (
    <div className="search-wrap" role="search">
      <div className={`search ${showDropdown ? 'is-open' : ''}`}>
        <Icon name="search" className="search-icon" />
        <input
          ref={inputRef}
          value={q}
          onChange={e => { setQ(e.target.value); setOpen(true); }}
          onFocus={() => setOpen(true)}
          onBlur={() => setTimeout(() => setOpen(false), 120)}
          onKeyDown={onKeyDown}
          placeholder="Search assets, projects, recorders…"
          type="search"
          role="combobox"
          aria-label="Search assets, projects, recorders, jobs, and users"
          aria-expanded={showDropdown}
          aria-controls={listboxId}
          aria-autocomplete="list"
          aria-activedescendant={showDropdown && results[sel] ? `${listboxId}-opt-${sel}` : undefined}
        />
        <span className="kbd" aria-hidden="true">{isMac ? '⌘K' : 'Ctrl K'}</span>
      </div>
      {showDropdown && (
        <div
          ref={listRef}
          id={listboxId}
          role="listbox"
          className="search-results"
          onMouseDown={(e) => e.preventDefault()}
        >
          {results.length === 0 ? (
            <div className="search-empty">No results for "{q}"</div>
          ) : results.map((r, i) => (
            <div
              key={r.kind + '-' + i + '-' + (r.item ? (r.item.id || r.item.name) : (r.target || r.label))}
              id={`${listboxId}-opt-${i}`}
              role="option"
              aria-selected={i === sel}
              className={`search-result ${i === sel ? 'active' : ''}`}
              onClick={() => handleSelect(r)}
              onMouseEnter={() => setSel(i)}
            >
              <span
                className="search-result-icon"
                style={r.color ? { color: r.color } : null}
              >
                <Icon name={r.icon} size={14} />
              </span>
              <div className="search-result-text">
                <div className="search-result-label">{r.label}</div>
                {r.sub && <div className="search-result-sub">{r.sub}</div>}
              </div>
              <span className={`search-result-kind kind-${r.kind}`}>{r.kind}</span>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

function Topbar({ crumbs, onNavigate, right, onOpenAsset, onOpenProject, onToggleSidebar }) {
  // Light cluster ping - the badge in the topbar should reflect reality,
  // not just look reassuring. /metrics/home returns cluster online/total.
  const [clusterHealthy, setClusterHealthy] = React.useState(true);
  React.useEffect(() => {
    let cancelled = false;
    const ping = () => {
      window.ZAMPP_API.fetch('/metrics/home?hours=1')
        .then(d => {
          if (cancelled) return;
          const c = d?.cards?.cluster;
          setClusterHealthy(!c || c.online >= c.total);
        })
        .catch(() => {});
    };
    ping();
    const t = setInterval(ping, 30_000);
    return () => { cancelled = true; clearInterval(t); };
  }, []);

  return (
    <header className="topbar">
      {onToggleSidebar && (
        <button
          className="icon-btn topbar-menu"
          onClick={onToggleSidebar}
          aria-label="Toggle sidebar"
          title="Toggle sidebar"
        >
          <Icon name="list" size={16} />
        </button>
      )}
      <div className="crumb">
        {crumbs.map((c, i) => (
          <React.Fragment key={i}>
            {i > 0 && <Icon name="chevron" size={12} className="sep" />}
            <span
              className={i === crumbs.length - 1 ? "current" : ""}
              style={{ cursor: c.to && i < crumbs.length - 1 ? "pointer" : "default" }}
              onClick={() => c.to && onNavigate && onNavigate(c.to)}
            >
              {c.label}
            </span>
          </React.Fragment>
        ))}
      </div>
      <div className="spacer" />
      <GlobalSearch onNavigate={onNavigate} onOpenAsset={onOpenAsset} onOpenProject={onOpenProject} />
      <div className="status-pip" title={clusterHealthy ? 'All nodes online' : 'One or more nodes offline'}>
        <span className="dot" style={{ background: clusterHealthy ? 'var(--success)' : 'var(--warning)' }} />
        <span>{clusterHealthy ? 'cluster healthy' : 'cluster degraded'}</span>
      </div>
      {right}
    </header>
  );
}

// General-purpose read-only form field used by recorder modal and other forms.
// Renders as a labeled select (when select=true) or text input.
function Field({ label, value, select, children }) {
  return (
    <div className="field">
      <label className="field-label">{label}</label>
      {children || (select
        ? (
          <select className="field-input" defaultValue={value} style={{ appearance: 'auto' }}>
            <option value={value}>{value}</option>
          </select>
        ) : (
          <input className="field-input" defaultValue={value} readOnly />
        )
      )}
    </div>
  );
}

window.Sidebar = Sidebar;
window.Topbar = Topbar;
window.NAV_TREE = NAV_TREE;
window.NAV_FLAT = NAV_FLAT;
window.NAV_SECTIONS = NAV_SECTIONS;
window.Field = Field;
window.GlobalSearch = GlobalSearch;
