/* ============================================================
   HOME PAGE
   ============================================================ */

function Home({state, idx, go, user}){
  const greeting = (() => {
    const h = new Date().getHours();
    return h < 12 ? 'Good morning' : h < 18 ? 'Good afternoon' : 'Good evening';
  })();
  const firstName = (user.name || '').split(' ')[0];

  const myRequests = state.requests.filter(r =>
    !['Done','Rejected'].includes(r.status) && (
      r.raisedByUserId === user.id
      || (user.personId && r.requesterPersonId === user.personId)
      || (user.personId && r.assignee && r.assignee.kind === 'person' && r.assignee.id === user.personId)
      || (user.personId && r.assignee && r.assignee.kind === 'role'
          && (idx.role[r.assignee.id]||{}).primaryPersonId === user.personId)));

  const myComponents = user.personId
    ? state.components.filter(c => OWNER_ROLES.some(o => {
        const ref = c.owners[o.key];
        if (!ref) return false;
        if (ref.kind === 'person') return ref.id === user.personId;
        return (idx.role[ref.id]||{}).primaryPersonId === user.personId;
      }))
    : [];

  const attention = [];
  state.components.filter(c => c.status !== 'Archived').forEach(c => {
    const missing = OWNER_ROLES.filter(o => !c.owners[o.key]);
    if (missing.length) attention.push({
      kind:'Ownership gap', title:c.name, detail:missing.map(m=>m.label).join(', '),
      tone:'amber', go:()=>go({view:'component', id:c.id})
    });
  });
  state.documents.forEach(d => {
    if (isOverdue(d.reviewDue) && d.status !== 'Retired') attention.push({
      kind:'Document overdue', title:d.title, detail:'Review was due ' + fmtDate(d.reviewDue),
      tone:'red', go:()=>go({view:'documents'})
    });
  });
  state.requests.forEach(r => {
    if (isOverdue(r.targetDate) && !['Done','Rejected'].includes(r.status)) attention.push({
      kind:'Request past target', title:`${r.reference} ${r.title}`,
      detail:'Target was ' + fmtDate(r.targetDate), tone:'red', go:()=>go({view:'request', id:r.id})
    });
  });
  state.reports.forEach(r => {
    if (isOverdue(r.nextDue) && r.status === 'Active') attention.push({
      kind:'Report past due', title:r.name, detail:'Due ' + fmtDate(r.nextDue),
      tone:'amber', go:()=>go({view:'reports'})
    });
  });
  state.envItems.forEach(e => {
    if (e.status !== 'Active') return;
    const n = daysUntil(e.expiresAt);
    if (n != null && n <= 30) attention.push({
      kind: n < 0 ? 'Environment item expired' : 'Environment item expiring soon', title:e.name,
      detail: n < 0 ? `Expired ${-n}d ago` : `Expires in ${n}d`,
      tone: n < 0 ? 'red' : 'amber', go:()=>go({view:'observability'})
    });
  });
  [...new Set(state.monitorTargets.filter(t=>t.enabled && t.status==='Down').map(t=>t.componentId))].forEach(cid => {
    const c = idx.comp[cid];
    if (!c) return;
    attention.push({
      kind:'Component down', title:c.name, detail:'A monitored URL is not responding',
      tone:'red', go:()=>go({view:'component', id:cid})
    });
  });

  // A single feed of the newest thing from each module.
  const feed = [
    ...state.requests.map(r => ({
      at:r.createdAt, icon:<I.inbox size={14}/>, tone:'amber',
      title:`${r.reference} ${r.title}`, sub:`Request raised by ${r.raisedBy}`,
      go:()=>go({view:'request', id:r.id})
    })),
    ...state.components.flatMap(c => (c.componentNotes||[]).map(n => ({
      at:n.createdAt, icon:<I.note size={14}/>, tone:'indigo',
      title:n.title || n.body.slice(0,60), sub:`Note on ${c.name} by ${n.createdBy}`,
      go:()=>go({view:'component', id:c.id})
    }))),
    ...state.documents.map(d => ({
      at:d.updatedAt, icon:<I.book size={14}/>, tone:'violet',
      title:d.title, sub:`Document updated by ${d.updatedBy}`,
      go:()=>go({view:'documents'})
    }))
  ].filter(x => x.at).sort((a,b) => b.at.localeCompare(a.at)).slice(0, 10);

  return (
    <div className="stack" style={{gap:20}}>
      <div className="home-hero">
        <div style={{flex:1,minWidth:240}}>
          <h1>{greeting}{firstName ? ', ' + firstName : ''}</h1>
          <p className="muted" style={{margin:'4px 0 0'}}>
            {myRequests.length > 0
              ? `You have ${myRequests.length} open request${myRequests.length===1?'':'s'}`
              : 'Nothing is waiting on you right now'}
            {myComponents.length > 0 && ` · you own ${myComponents.length} component${myComponents.length===1?'':'s'}`}
            {attention.length > 0 && ` · ${attention.length} item${attention.length===1?'':'s'} need attention`}
          </p>
        </div>
      </div>

      <div className="module-grid">
        {state.modules.map(m => {
          const Icon = moduleIconFor(m.icon);
          return (
            <div className="module-card" key={m.key} onClick={()=>go({view:m.key})}>
              <div className="row" style={{gap:12,alignItems:'flex-start'}}>
                <div className={'module-icon ' + (m.accent || 'indigo')}><Icon size={20}/></div>
                <div style={{minWidth:0,flex:1}}>
                  <div className="module-name">{m.name}</div>
                  <div className="module-tagline">{m.tagline}</div>
                </div>
                <span className="dim"><I.chev size={16}/></span>
              </div>
              <div className="module-desc">{m.description}</div>
            </div>
          );
        })}
      </div>

      <div className="grid" style={{gridTemplateColumns:'repeat(auto-fit,minmax(340px,1fr))'}}>
        <div className="card">
          <div className="card-head"><h3>Waiting on you</h3>
            <Badge tone={myRequests.length ? 'accent' : 'green'}>{myRequests.length}</Badge>
            <div className="spacer"/>
            <button className="btn sm ghost" onClick={()=>go({view:'requests'})}>All requests</button></div>
          <div className="card-body tight scroll-y" style={{maxHeight:290}}>
            {myRequests.length === 0
              ? <EmptyState icon={<I.check size={26}/>} title="Nothing assigned to you"/>
              : myRequests.slice(0,8).map(r =>
                  <div className="feed-row" key={r.id} onClick={()=>go({view:'request', id:r.id})}>
                    <Priority value={r.priority}/>
                    <div style={{flex:1,minWidth:0}}>
                      <div style={{fontWeight:600}} className="truncate">{r.title}</div>
                      <div className="cell-sub">
                        <span className="mono">{r.reference}</span> · {r.status}
                        {r.targetDate ? ' · target ' + fmtDate(r.targetDate) : ''}</div>
                    </div>
                  </div>)}
          </div>
        </div>

        <div className="card">
          <div className="card-head"><h3>Needs attention</h3>
            <Badge tone={attention.length ? 'amber' : 'green'}>{attention.length}</Badge></div>
          <div className="card-body tight scroll-y" style={{maxHeight:290}}>
            {attention.length === 0
              ? <EmptyState icon={<I.check size={26}/>} title="Everything is up to date"/>
              : attention.slice(0,10).map((a,i) =>
                  <div className="feed-row" key={i} onClick={a.go}>
                    <Badge tone={a.tone}>{a.kind}</Badge>
                    <div style={{flex:1,minWidth:0}}>
                      <div style={{fontWeight:600}} className="truncate">{a.title}</div>
                      <div className="cell-sub">{a.detail}</div>
                    </div>
                  </div>)}
          </div>
        </div>

        <div className="card">
          <div className="card-head"><h3>Recent activity</h3></div>
          <div className="card-body tight scroll-y" style={{maxHeight:290}}>
            {feed.length === 0
              ? <EmptyState icon={<I.history size={26}/>} title="Nothing has happened yet"/>
              : feed.map((f,i) =>
                  <div className="feed-row" key={i} onClick={f.go}>
                    <span className={'module-icon ' + f.tone}
                          style={{width:28,height:28,flexBasis:28,borderRadius:8}}>{f.icon}</span>
                    <div style={{flex:1,minWidth:0}}>
                      <div style={{fontWeight:600}} className="truncate">{f.title}</div>
                      <div className="cell-sub">{f.sub} · {relative(f.at)}</div>
                    </div>
                  </div>)}
          </div>
        </div>

        {myComponents.length > 0 &&
          <div className="card">
            <div className="card-head"><h3>Components you own</h3>
              <Badge tone="slate">{myComponents.length}</Badge></div>
            <div className="card-body tight scroll-y" style={{maxHeight:290}}>
              {myComponents.map(c =>
                <div className="feed-row" key={c.id} onClick={()=>go({view:'component', id:c.id})}>
                  <TierBadge tier={c.tier}/>
                  <div style={{flex:1,minWidth:0}}>
                    <div style={{fontWeight:600}} className="truncate">{c.name}</div>
                    <div className="cell-sub">{(idx.type[c.typeId]||{}).name} · {c.status}</div>
                  </div>
                </div>)}
            </div>
          </div>}
      </div>
    </div>
  );
}

/* ============================================================
   APP SHELL
   ============================================================ */

const NEW_COMPONENT = () => ({
  id:'', identifier:'', name:'', description:'', typeId:'', status:'Active', tier:3,
  owners:{executive:null, business:null, technical:null, sysadmin:null},
  tags:[], vendor:'', environment:'Production', hostingLocation:'', businessCapability:'',
  goLiveDate:'', lastReviewed:'', costCentre:'', recoveryTier:'', notes:''
});

const MODULE_VIEWS = ['components','directory','tools','documents','requests','changes','reports','observability'];

const DIRECTORY_CREATE_LABEL = {
  people:'Add person', roles:'Add role', contacts:'Add contact'
};

/** List modules manage their own scrolling: the filter header stays put and only
 * the results region scrolls. Record pages and settings keep normal page scroll. */
const FILL_VIEWS = MODULE_VIEWS;

function routeFromHash(){
  const raw = window.location.hash || '';
  const [pathPart, queryPart] = raw.replace(/^#\/?/, '').split('?');
  const parts = pathPart.split('/').filter(Boolean);
  const query = new URLSearchParams(queryPart || '');
  const extra = {};
  if (query.get('mine')) extra.filter = 'mine';
  if (query.get('tier')) extra.tier = query.get('tier');
  if (query.get('overdue')) extra.overdue = '1';
  if (query.get('tab')) extra.tab = query.get('tab');
  if (query.get('view')) extra.subview = query.get('view');

  if (!parts.length) return {view:'home', ...extra};
  if (parts[0] === 'component' && parts[1]) return {view:'component', id:parts[1], ...extra};
  if (parts[0] === 'request' && parts[1]) return {view:'request', id:parts[1], ...extra};
  if (parts[0] === 'change' && parts[1]) return {view:'change', id:parts[1], ...extra};
  // Back-compat: `#/changes/:id` and `#/requests/:id` used to open a modal over the
  // list. They still resolve, straight to the full record page.
  if (parts[0] === 'changes' && parts[1] && parts[1] !== 'new') return {view:'change', id:parts[1], ...extra};
  if (parts[0] === 'requests' && parts[1] && parts[1] !== 'new') return {view:'request', id:parts[1], ...extra};
  if (parts[0] === 'directory') return {view:'directory', tab: parts[1] || 'people', ...extra};
  if (MODULE_VIEWS.includes(parts[0]) && parts[1] === 'new') return {view:parts[0], action:'new', ...extra};
  return {view: parts[0] || 'home', ...extra};
}
function hashFromRoute(r){
  if (r.view === 'component') return '#/component/' + r.id;
  if (r.view === 'request') return '#/request/' + r.id;
  if (r.view === 'change') return '#/change/' + r.id;
  if (r.view === 'directory') return '#/directory/' + (r.tab || 'people');
  if (r.view === 'home') return '#/';
  if (r.action === 'new') return `#/${r.view}/new`;
  return '#/' + r.view;
}

/** Deep-link entry points like /components/SVC-003 — a friendly path that
 * resolves a record's public identifier to its internal id, once state has
 * loaded, then hands off to the normal hash router. */
const DEEP_LINK_MODULES = {
  components: {view:'component', find:(state, ident)=>state.components.find(c=>(c.identifier||'').toLowerCase()===ident)},
  requests:   {view:'request',   find:(state, ident)=>state.requests.find(r=>(r.reference||'').toLowerCase()===ident)},
  changes:    {view:'change',    find:(state, ident)=>state.changes.find(c=>(c.reference||'').toLowerCase()===ident)}
};
function pathDeepLink(){
  const parts = window.location.pathname.split('/').filter(Boolean).map(decodeURIComponent);
  if (parts.length !== 2) return null;
  const spec = DEEP_LINK_MODULES[parts[0]];
  return spec ? {spec, ident: parts[1].toLowerCase(), label: parts[1]} : null;
}

/** The home page's "Create" button: one primary action that opens a menu of
 * every record type the signed-in user is allowed to raise, so the topbar
 * doesn't need to guess which module they're headed for. */
function CreateMenu({items}){
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  useEffect(()=>{
    if (!open) return;
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', h);
    return ()=>document.removeEventListener('mousedown', h);
  }, [open]);
  if (!items.length) return null;
  return (
    <div className="acct" ref={ref}>
      <button className="btn primary" onClick={()=>setOpen(o=>!o)}>
        <I.plus size={14}/>Create<I.chev size={13}/></button>
      {open &&
        <div className="acct-menu">
          {items.map(it => {
            const Icon = moduleIconFor(it.icon);
            return (
              <button key={it.key} className="acct-item" onClick={()=>{ setOpen(false); it.action(); }}>
                <Icon size={15}/>{it.label}</button>
            );
          })}
        </div>}
    </div>
  );
}

/** Names and saves the current URL (module + filters, already live-synced by
 * each module's own useFilterUrlSync) as a personal entry in the sidebar. */
function SaveViewModal({defaultLabel, onClose, onSave}){
  const [label, setLabel] = useState(defaultLabel);
  const [busy, setBusy] = useState(false);
  return (
    <Modal title="Save this view" onClose={onClose} footer={<>
      <button className="btn" onClick={onClose} disabled={busy}>Cancel</button>
      <button className="btn primary" disabled={!label.trim() || busy}
        onClick={async ()=>{ setBusy(true); try { await onSave(label.trim()); } finally { setBusy(false); } }}>
        {busy ? 'Saving…' : 'Save view'}</button>
    </>}>
      <Field label="Name" hint="Shown in the sidebar under Saved views, with the filters currently applied.">
        <input type="text" value={label} onChange={e=>setLabel(e.target.value)} autoFocus
          placeholder="e.g. My open P1 requests"/>
      </Field>
    </Modal>
  );
}

function AccountMenu({user, theme, onTheme, onChangePassword, onProfile, onSignOut}){
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  useEffect(()=>{
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', h);
    return ()=>document.removeEventListener('mousedown', h);
  },[]);
  const THEMES = [
    {k:'light', label:'Light', icon:<I.sun size={15}/>},
    {k:'dark', label:'Dark', icon:<I.moon size={15}/>},
    {k:'system', label:'System', icon:<I.monitor size={15}/>}
  ];
  return (
    <div className="acct" ref={ref}>
      <button className="acct-btn" onClick={()=>setOpen(o=>!o)}>
        <Avatar name={user.name} userId={user.id} hasAvatar={user.hasAvatar}/>
        <span style={{textAlign:'left',lineHeight:1.2}}>
          <span style={{display:'block',fontWeight:600,fontSize:13}}>{user.name}</span>
          <span className="xsmall dim">{ROLE_LABEL[user.role]}</span>
        </span>
        <I.chev size={14}/>
      </button>
      {open &&
        <div className="acct-menu">
          <div className="acct-head">
            <div className="row" style={{gap:10}}>
              <Avatar name={user.name} size="lg" userId={user.id} hasAvatar={user.hasAvatar}/>
              <div style={{minWidth:0}}>
                <div style={{fontWeight:650}}>{user.name}</div>
                <div className="xsmall dim truncate">{user.email}</div>
              </div>
            </div>
          </div>
          <div className="theme-row">
            {THEMES.map(t =>
              <button key={t.k} className={'theme-btn' + (theme===t.k ? ' on' : '')}
                onClick={()=>onTheme(t.k)}>{t.icon}{t.label}</button>)}
          </div>
          <button className="acct-item" onClick={()=>{ setOpen(false); onProfile(); }}>
            <I.users size={15}/>My profile</button>
          <button className="acct-item" onClick={()=>{ setOpen(false); onChangePassword(); }}>
            <I.key size={15}/>Change password</button>
          <button className="acct-item" onClick={()=>{ setOpen(false); onSignOut(); }}>
            <I.logout size={15}/>Sign out</button>
        </div>}
    </div>
  );
}

function Workspace({user, setUser, onSignedOut}){
  const [state, setState] = useState(null);
  const [loadError, setLoadError] = useState('');
  const [route, setRoute] = useState(routeFromHash);
  const [dirTab, setDirTab] = useState(() => routeFromHash().tab || 'people');
  const [editing, setEditing] = useState(null);
  const [toasts, setToasts] = useState([]);
  const toastIdRef = useRef(0);
  const [theme, setTheme] = useState(() => bootTheme());
  const [mustChangePassword, setMustChangePassword] = useState(!!user.mustChangePassword);
  const [pwModal, setPwModal] = useState(!!user.mustChangePassword);
  const [paletteOpen, setPaletteOpen] = useState(false);
  const [sidebarCollapsed, setSidebarCollapsed] = useState(()=>{
    try { return window.localStorage.getItem('fuse.sidebarCollapsed') === '1'; } catch { return false; }
  });
  const [mobileNavOpen, setMobileNavOpen] = useState(false);
  const [savingView, setSavingView] = useState(false);
  const toggleSidebar = useCallback(()=>{
    setSidebarCollapsed(v => {
      const next = !v;
      try { window.localStorage.setItem('fuse.sidebarCollapsed', next ? '1' : '0'); } catch { /* private mode */ }
      return next;
    });
  }, []);

  const dismissToast = useCallback((id)=>{
    setToasts(list => list.filter(t => t.id !== id));
  }, []);

  /** Queues a toast rather than replacing whatever is already showing, so a
   * quick run of actions doesn't drop earlier messages. `action` is optional:
   * {label, onClick} adds a button (used for undo), and keeps the toast up
   * longer so there's time to click it. */
  const toast = useCallback((text, bad, action)=>{
    const id = ++toastIdRef.current;
    const duration = action ? 6000 : (bad ? 5000 : 2200);
    setToasts(list => [...list, {id, text, bad, action}]);
    setTimeout(()=>dismissToast(id), duration);
    return id;
  },[dismissToast]);

  const refresh = useCallback(async () => {
    try {
      const next = await API.get('/api/state');
      setState(next);
      setLoadError('');
    } catch (err) {
      if (err.status === 401) { onSignedOut(); return; }
      setLoadError(err.message);
    }
  }, [onSignedOut]);

  const refreshUser = useCallback(async () => {
    try {
      const res = await API.get('/api/auth/me');
      if (res.user) setUser(res.user);
    } catch { /* the next state fetch will catch a lost session */ }
  }, [setUser]);

  useEffect(()=>{ refresh(); }, [refresh]);

  // Apply the theme the server remembers for this user, once we know it.
  useEffect(()=>{
    if (user.theme && user.theme !== theme) { setTheme(user.theme); applyTheme(user.theme); }
  }, [user.theme]);

  useEffect(()=>{
    const media = window.matchMedia('(prefers-color-scheme: dark)');
    const h = () => { if (theme === 'system') applyTheme('system'); };
    media.addEventListener('change', h);
    return () => media.removeEventListener('change', h);
  }, [theme]);

  const chooseTheme = async (next) => {
    setTheme(next);
    applyTheme(next);
    try { await API.put('/api/auth/preferences', {theme: next}); } catch { /* stays local */ }
  };

  /** Run a mutation, refresh, and report the outcome. */
  const act = useCallback(async (promise, successMessage) => {
    try {
      await promise;
      await refresh();
      if (successMessage) toast(successMessage);
      return true;
    } catch (err) {
      if (err.status === 401) { onSignedOut(); return false; }
      toast(err.message, true);
      return false;
    }
  }, [refresh, toast, onSignedOut]);

  const removeSavedView = useCallback((id)=>{
    deleteWithUndo(act, toast, `/api/saved-views/${id}`, 'View');
  }, [act, toast]);

  useEffect(()=>{
    const h = () => setRoute(routeFromHash());
    window.addEventListener('hashchange', h);
    return ()=>window.removeEventListener('hashchange', h);
  },[]);
  // Directory's sub-tab lives in its own state (so switching tabs doesn't
  // remount the whole module), but deep links like #/directory/capacities
  // still need to land on the right tab even when Directory is already mounted.
  useEffect(()=>{
    if (route && route.view === 'directory' && route.tab) setDirTab(route.tab);
  }, [route]);
  useEffect(()=>{
    const h = (e) => {
      if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
        e.preventDefault();
        setPaletteOpen(o => !o);
      }
    };
    window.addEventListener('keydown', h);
    return ()=>window.removeEventListener('keydown', h);
  },[]);
  useEffect(()=>{
    // Compares paths only, ignoring the query string: a module's own filters
    // (useFilterUrlSync) mirror themselves onto that query string, and this
    // reconciliation shouldn't fight that on every unrelated route change.
    const wantPath = hashFromRoute(route).split('?')[0];
    const curPath = window.location.hash.split('?')[0];
    if (curPath !== wantPath && !route.filter && !route.tier && !route.overdue && !route.subview) {
      window.history.replaceState(null, '', wantPath);
    }
  },[route]);

  const go = useCallback((r)=>{
    setRoute(r);
    if (r.tab && r.view === 'directory') setDirTab(r.tab);
    window.history.replaceState(null, '', hashFromRoute(r));
    const el = document.querySelector('.content'); if (el) el.scrollTop = 0;
    setMobileNavOpen(false);
  },[]);

  const idx = useMemo(()=> state ? makeIndex(state) : null, [state]);
  const canEditComponent = useCallback((componentId) => {
    if (!state) return false;
    const ids = state.permissions.editableComponentIds;
    return ids === '*' || (Array.isArray(ids) && ids.includes(componentId));
  }, [state]);
  const canEditRequest = useCallback((requestId) => {
    if (!state) return false;
    const ids = state.permissions.editableRequestIds;
    return ids === '*' || (Array.isArray(ids) && ids.includes(requestId));
  }, [state]);
  const canEditChange = useCallback((changeId) => {
    if (!state) return false;
    const ids = state.permissions.editableChangeIds;
    return ids === '*' || (Array.isArray(ids) && ids.includes(changeId));
  }, [state]);

  // Pretty deep links like /components/SVC-003: resolve the identifier to an
  // internal id once, then hand off to the normal hash router.
  const deepLinkHandled = useRef(false);
  useEffect(()=>{
    if (!state || deepLinkHandled.current) return;
    deepLinkHandled.current = true;
    const link = pathDeepLink();
    if (!link) return;
    const found = link.spec.find(state, link.ident);
    window.history.replaceState(null, '', '/');
    if (found) go({view: link.spec.view, id: found.id});
    else toast(`No record found for "${link.label}"`, true);
  }, [state, go, toast]);

  const signOut = async () => {
    try { await API.post('/api/auth/logout', {}); } catch { /* sign out anyway */ }
    onSignedOut();
  };

  if (loadError) {
    return (
      <div className="loading-full" style={{flexDirection:'column'}}>
        <div className="err" style={{maxWidth:440}}>{loadError}</div>
        <button className="btn" onClick={refresh}><I.refresh size={14}/>Try again</button>
      </div>
    );
  }
  if (!state) return <div className="loading-full"><span className="spinner"/><span>Loading your workspace…</span></div>;

  const quickGroups = {};
  state.quickLinks.forEach(q => { (quickGroups[q.groupLabel] = quickGroups[q.groupLabel] || []).push(q); });

  const CONFIG_NAV = [
    {k:'profile', label:'My profile', icon:<I.users size={16}/>},
    {k:'data', label:'Data', icon:<I.gear size={16}/>}
  ];
  if (state.permissions.isAdmin){
    CONFIG_NAV.push({k:'workflow', label:'Change workflow', icon:<I.stamp size={16}/>});
    CONFIG_NAV.push({k:'quicklinks', label:'Quick links', icon:<I.link size={16}/>});
    CONFIG_NAV.push({k:'users', label:'Users', icon:<I.key size={16}/>});
    CONFIG_NAV.push({k:'audit', label:'Audit log', icon:<I.history size={16}/>});
  }

  const titles = {
    home:'Home', components:'Components Model', component:'Components Model',
    directory:'Directory', tools:'Tools & Services',
    documents:'Documents',
    requests:'Business Requests', changes:'Change Management', reports:'Reports',
    observability:'Observability & Monitoring',
    profile:'My profile',
    data:'Data & settings', workflow:'Change workflow', quicklinks:'Quick links',
    users:'User accounts', audit:'Audit log'
  };
  const moduleMeta = Object.fromEntries(state.modules.map(m => [m.key, m]));
  const current = idx.comp[route.id];
  const currentRequest = idx.request[route.id];
  const currentChange = idx.change[route.id];

  const navFor = (m) => {
    const counts = {
      components: state.components.length,
      directory: state.people.length + state.roles.length,
      tools: state.tools.length,
      documents: state.documents.length,
      requests: state.requests.filter(r => !['Done','Rejected'].includes(r.status)).length,
      changes: state.changes.filter(c => !['Closed','Cancelled','Rejected'].includes(c.status)).length,
      reports: state.reports.length,
      observability: state.envItems.filter(e => e.status==='Active' && daysUntil(e.expiresAt) <= 30).length
    };
    return counts[m.key];
  };

  // Every module's "create" button lives in the topbar rather than its own
  // toolbar, keyed off the current view (and, for Directory, the open tab).
  const canCreateTool = state.permissions.editableToolIds === '*'
    || state.permissions.editableComponentIds === '*'
    || (Array.isArray(state.permissions.editableComponentIds) && state.permissions.editableComponentIds.length > 0);
  const canCreateDocument = state.permissions.editableDocumentIds === '*'
    || state.permissions.editableComponentIds === '*'
    || (Array.isArray(state.permissions.editableComponentIds) && state.permissions.editableComponentIds.length > 0);
  const canCreateReport = state.permissions.editableReportIds === '*';
  const canCreateObservability = state.permissions.canEditObservability;
  const dirCreateLabel = DIRECTORY_CREATE_LABEL[dirTab];
  const canCreateDirectoryItem = state.permissions.canEditDirectory;

  const createItems = [
    state.permissions.canRaiseRequests && {key:'request', label:'Business request', icon:'inbox',
      action:()=>go({view:'requests', action:'new'})},
    {key:'change', label:'Change', icon:'change', action:()=>go({view:'changes', action:'new'})},
    state.permissions.canCreateComponents && {key:'component', label:'Component', icon:'grid',
      action:()=>{ go({view:'components'}); setEditing(NEW_COMPONENT()); }},
    canCreateDocument && {key:'document', label:'Document', icon:'book',
      action:()=>go({view:'documents', action:'new'})},
    canCreateTool && {key:'tool', label:'Tool or service', icon:'tool',
      action:()=>go({view:'tools', action:'new'})},
    canCreateReport && {key:'report', label:'Report', icon:'chart',
      action:()=>go({view:'reports', action:'new'})},
    canCreateObservability && {key:'envitem', label:'Environment item', icon:'pulse',
      action:()=>go({view:'observability', action:'new'})}
  ].filter(Boolean);

  return (
    <div className="app">
      {mobileNavOpen && <div className="sidebar-backdrop" onClick={()=>setMobileNavOpen(false)}/>}
      <aside className={'sidebar' + (sidebarCollapsed ? ' collapsed' : '') + (mobileNavOpen ? ' mobile-open' : '')}>
        <div className="brand">
          <div className="brand-click" onClick={()=>go({view:'home'})}>
            <div className="brand-mark">
              <svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <circle cx="12" cy="5" r="2.4"/><circle cx="5" cy="18" r="2.4"/><circle cx="19" cy="18" r="2.4"/>
                <path d="M10.5 7 6.4 15.7M13.5 7l4.1 8.7M7.6 18h8.8"/>
              </svg>
            </div>
            <div className="brand-text">
              <div className="brand-name">Fuse</div>
              <div className="brand-sub">Business workspace</div>
            </div>
          </div>
          <button className="sidebar-toggle-btn" onClick={toggleSidebar}
            title={sidebarCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}>
            {sidebarCollapsed ? <I.arrowR size={15}/> : <I.arrowL size={15}/>}
          </button>
        </div>

        <div className="nav-group">
          <button className={'nav-item' + (route.view==='home' ? ' active':'')} onClick={()=>go({view:'home'})}>
            <span className="ico"><I.home size={16}/></span><span className="lbl">Home</span>
          </button>

          <div className="nav-label">Modules</div>
          {state.modules.map(m => {
            const Icon = moduleIconFor(m.icon);
            const active = route.view === m.key
              || (m.key === 'components' && route.view === 'component')
              || (m.key === 'requests' && route.view === 'request')
              || (m.key === 'changes' && route.view === 'change');
            return (
              <button key={m.key} className={'nav-item' + (active ? ' active':'')}
                onClick={()=>go({view:m.key, tab: m.key==='directory' ? dirTab : undefined})}>
                <span className="ico"><Icon size={16}/></span>
                <span className="lbl">{m.name}</span>
                <span className="nav-count">{navFor(m)}</span>
              </button>
            );
          })}

          {Object.keys(quickGroups).sort().map(group => (
            <React.Fragment key={group}>
              <div className="nav-label">{group}</div>
              {quickGroups[group].map(q => {
                const Icon = I[q.icon] || I.link;
                const external = /^https?:\/\//i.test(q.target);
                return external
                  ? <a key={q.id} className="nav-item quick" href={q.target} target="_blank"
                       rel="noopener noreferrer" title={q.description}>
                      <span className="ico"><Icon size={15}/></span>
                      <span className="lbl">{q.label}</span>
                      <span className="nav-count"><I.link size={11}/></span>
                    </a>
                  : <button key={q.id} className="nav-item quick" title={q.description}
                      onClick={()=>{ window.location.hash = q.target.replace(/^#/, ''); setRoute(routeFromHash()); }}>
                      <span className="ico"><Icon size={15}/></span>
                      <span className="lbl">{q.label}</span>
                    </button>;
              })}
            </React.Fragment>
          ))}

          {state.savedViews.length > 0 &&
            <>
              <div className="nav-label">Saved views</div>
              {state.savedViews.map(v => {
                const Icon = I[v.icon] || I.link;
                return (
                  <div className="nav-saved" key={v.id}>
                    <button className="nav-item quick" title={v.label}
                      onClick={()=>{ window.location.hash = v.target.replace(/^#/, ''); setRoute(routeFromHash()); }}>
                      <span className="ico"><Icon size={15}/></span>
                      <span className="lbl">{v.label}</span>
                    </button>
                    <button className="nav-remove" title="Remove this saved view"
                      onClick={()=>removeSavedView(v.id)}>
                      <I.x size={12}/></button>
                  </div>
                );
              })}
            </>}

          <div className="nav-label">Configuration</div>
          {CONFIG_NAV.map(n =>
            <button key={n.k} className={'nav-item' + (route.view===n.k ? ' active':'')}
              onClick={()=>go({view:n.k})}>
              <span className="ico">{n.icon}</span><span className="lbl">{n.label}</span>
            </button>)}
        </div>

        <div className="sidebar-foot">
          Signed in as {ROLE_LABEL[user.role].toLowerCase()}.<br/>
          <span style={{opacity:.8}}>{state.modules.length} modules · {state.components.length} components</span>
        </div>
      </aside>

      <main className="main">
        <div className="topbar">
          <button className="icon-btn mobile-nav-btn" title="Open menu" onClick={()=>setMobileNavOpen(true)}>
            <I.list size={18}/></button>
          <button className="icon-btn sidebar-expand-btn" title="Expand sidebar" onClick={toggleSidebar}>
            <I.list size={18}/></button>
          {route.view === 'component'
            ? <div className="crumb">
                <a onClick={()=>go({view:'components'})}>Components Model</a>
                <I.chev size={13}/>
                <strong>{current ? current.name : 'Not found'}</strong>
              </div>
            : route.view === 'request'
            ? <div className="crumb">
                <a onClick={()=>go({view:'requests'})}>Business Requests</a>
                <I.chev size={13}/>
                <strong>{currentRequest ? currentRequest.reference : 'Not found'}</strong>
              </div>
            : route.view === 'change'
            ? <div className="crumb">
                <a onClick={()=>go({view:'changes'})}>Change Management</a>
                <I.chev size={13}/>
                <strong>{currentChange ? currentChange.reference : 'Not found'}</strong>
              </div>
            : <div>
                <h1>{titles[route.view] || 'Fuse'}</h1>
                {moduleMeta[route.view] &&
                  <div className="xsmall dim">{moduleMeta[route.view].tagline}</div>}
              </div>}
          <div className="spacer"/>
          {route.view === 'component' &&
            <button className="btn ghost sm" onClick={()=>go({view:'components'})}><I.arrowL size={14}/>Back</button>}
          {route.view === 'request' &&
            <button className="btn ghost sm" onClick={()=>go({view:'requests'})}><I.arrowL size={14}/>Back</button>}
          {route.view === 'change' &&
            <button className="btn ghost sm" onClick={()=>go({view:'changes'})}><I.arrowL size={14}/>Back</button>}
          {route.view === 'home' && <CreateMenu items={createItems}/>}
          {state.permissions.canCreateComponents
            && (route.view === 'component' || route.view === 'components') &&
            <button className="btn primary" onClick={()=>setEditing(NEW_COMPONENT())}>
              <I.plus size={14}/>New component</button>}
          {route.view === 'directory' && canCreateDirectoryItem &&
            <button className="btn primary" onClick={()=>go({view:'directory', tab:dirTab, action:'new'})}>
              <I.plus size={14}/>{dirCreateLabel}</button>}
          {route.view === 'tools' && canCreateTool &&
            <button className="btn primary" onClick={()=>go({view:'tools', action:'new'})}>
              <I.plus size={14}/>New tool</button>}
          {route.view === 'documents' && canCreateDocument &&
            <button className="btn primary" onClick={()=>go({view:'documents', action:'new'})}>
              <I.plus size={14}/>New document</button>}
          {route.view === 'requests' && state.permissions.canRaiseRequests &&
            <button className="btn primary" onClick={()=>go({view:'requests', action:'new'})}>
              <I.plus size={14}/>Raise a request</button>}
          {route.view === 'changes' &&
            <button className="btn primary" onClick={()=>go({view:'changes', action:'new'})}>
              <I.plus size={14}/>Raise a change</button>}
          {route.view === 'reports' && canCreateReport &&
            <button className="btn primary" onClick={()=>go({view:'reports', action:'new'})}>
              <I.plus size={14}/>New report</button>}
          {route.view === 'observability' && canCreateObservability &&
            <button className="btn primary" onClick={()=>go({view:'observability', action:'new'})}>
              <I.plus size={14}/>New environment item</button>}
          {MODULE_VIEWS.includes(route.view) &&
            <button className="icon-btn" title="Save this view" onClick={()=>setSavingView(true)}>
              <I.pin size={16}/></button>}
          <button className="btn sm ghost cmdk-trigger" title="Search everything (Ctrl/Cmd+K)"
            onClick={()=>setPaletteOpen(true)}>
            <I.search size={13}/>Search <kbd>{navigator.platform.includes('Mac') ? '⌘K' : 'Ctrl K'}</kbd></button>
          <button className="icon-btn" title="Reload from the server" onClick={()=>refresh()}>
            <I.refresh size={16}/></button>
          <NotificationBell unread={state.unreadNotifications}
            onNavigate={(url)=>{ window.location.hash = url.replace(/^#/, ''); setRoute(routeFromHash()); }}
            refreshState={refresh}/>
          <AccountMenu user={user} theme={theme} onTheme={chooseTheme}
            onChangePassword={()=>setPwModal(true)} onProfile={()=>go({view:'profile'})}
            onSignOut={signOut}/>
        </div>

        <div className={'content' + (FILL_VIEWS.includes(route.view) ? ' fill' : '')}>
          <div className="content-inner">
            {route.view === 'home' && <Home state={state} idx={idx} go={go} user={user}/>}
            {route.view === 'components' && <ComponentsModule state={state} idx={idx} go={go}
              canCreate={state.permissions.canCreateComponents} onNew={()=>setEditing(NEW_COMPONENT())}
              act={act} toast={toast} route={route}/>}
            {route.view === 'component' && <ComponentDetail id={route.id} state={state} idx={idx} go={go}
              act={act} toast={toast} onEdit={setEditing} canEdit={canEditComponent}/>}
            {route.view === 'directory' && <Directory state={state} idx={idx} go={go} act={act} tab={dirTab}
              setTab={(t)=>{ setDirTab(t); go({view:'directory', tab:t}); }} toast={toast} route={route}/>}
            {route.view === 'tools' && <ToolsModule state={state} idx={idx} go={go}
              act={act} toast={toast} route={route}/>}
            {route.view === 'documents' && <DocumentsModule state={state} idx={idx} go={go}
              act={act} toast={toast} route={route}/>}
            {route.view === 'requests' && <RequestsModule state={state} idx={idx} go={go}
              act={act} toast={toast} user={user} route={route}/>}
            {route.view === 'request' && <RequestDetail id={route.id} state={state} idx={idx} go={go}
              act={act} toast={toast} user={user} canEdit={canEditRequest}/>}
            {route.view === 'changes' && <ChangesModule state={state} idx={idx} go={go}
              act={act} toast={toast} user={user} route={route}/>}
            {route.view === 'change' && <ChangeDetail id={route.id} state={state} idx={idx} go={go}
              act={act} toast={toast} user={user} canEdit={canEditChange}/>}
            {route.view === 'reports' && <ReportsModule state={state} idx={idx} go={go}
              act={act} toast={toast} route={route}/>}
            {route.view === 'observability' && <ObservabilityModule state={state} idx={idx} go={go}
              act={act} toast={toast} route={route}/>}
            {route.view === 'profile' && <MyProfile state={state} user={user}
              refreshUser={refreshUser} toast={toast} act={act}/>}
            {route.view === 'data' && <DataSettings state={state} act={act} toast={toast}/>}
            {route.view === 'workflow' && <WorkflowAdmin state={state} idx={idx} act={act} toast={toast}/>}
            {route.view === 'quicklinks' && (state.permissions.isAdmin
              ? <QuickLinksAdmin state={state} act={act}/>
              : <EmptyState icon={<I.warn size={30}/>} title="Administrators only"/>)}
            {route.view === 'users' && (state.permissions.isAdmin
              ? <Users state={state} act={act} toast={toast} currentUser={user}/>
              : <EmptyState icon={<I.warn size={30}/>} title="Administrators only"/>)}
            {route.view === 'audit' && (state.permissions.isAdmin
              ? <AuditLog toast={toast}/>
              : <EmptyState icon={<I.warn size={30}/>} title="Administrators only"/>)}
          </div>
        </div>
      </main>

      {editing &&
        <ComponentForm value={editing} state={state} onClose={()=>setEditing(null)} toast={toast}
          onSave={async (c)=>{
            const isNew = !c.id;
            try {
              const res = isNew ? await API.post('/api/components', c) : await API.put(`/api/components/${c.id}`, c);
              await refresh();
              setEditing(null);
              toast(isNew ? 'Component created' : 'Changes saved');
              if (isNew && res && res.id) go({view:'component', id:res.id});
            } catch (err) {
              if (err.status === 401) { onSignedOut(); return; }
              toast(err.message, true);
            }
          }}/>}

      {pwModal &&
        <ChangePasswordModal forced={mustChangePassword} toast={toast}
          onClose={()=>{ setMustChangePassword(false); setPwModal(false); }}/>}

      {paletteOpen && <CommandPalette state={state} idx={idx} go={go} onClose={()=>setPaletteOpen(false)}/>}

      {savingView &&
        <SaveViewModal defaultLabel={titles[route.view] || 'View'} onClose={()=>setSavingView(false)}
          onSave={async (label)=>{
            const icon = (moduleMeta[route.view] || {}).icon || 'link';
            const ok = await act(API.post('/api/saved-views',
              {label, target: window.location.hash || '#/', icon}), 'View saved');
            if (ok) setSavingView(false);
          }}/>}

      {toasts.length > 0 &&
        <div className="toast-stack">
          {toasts.map(t => (
            <div key={t.id} className={'toast' + (t.bad ? ' bad' : '')}>
              <span>{t.text}</span>
              {t.action &&
                <button className="toast-action" onClick={()=>{ t.action.onClick(); dismissToast(t.id); }}>
                  {t.action.label}</button>}
              <button className="toast-close" onClick={()=>dismissToast(t.id)} title="Dismiss">
                <I.x size={12}/></button>
            </div>
          ))}
        </div>}
    </div>
  );
}

function App(){
  const [user, setUser] = useState(undefined);   // undefined = still checking

  useEffect(()=>{
    bootTheme();
    API.get('/api/auth/me')
      .then(res => { setUser(res.user); if (res.user && res.user.theme) applyTheme(res.user.theme); })
      .catch(() => setUser(null));
  },[]);

  if (user === undefined) {
    return <div className="loading-full"><span className="spinner"/><span>Checking your session…</span></div>;
  }
  if (!user) return <LoginScreen onSignedIn={(u)=>{ setUser(u); if (u.theme) applyTheme(u.theme); }}/>;
  return <Workspace key={user.id} user={user} setUser={setUser} onSignedOut={()=>setUser(null)}/>;
}

ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
