/* ============================================================
   NeuroRoute — App shell: hash router + sidebar + topbar
   Pages register on window.PAGES; this file mounts root.
   ============================================================ */
const NAV = [
  {section:'Overview', items:[
    {id:'dashboard', label:'Dashboard',      icon:'dashboard'},
    {id:'usage',     label:'Usage Analytics',icon:'analytics'},
  ]},
  {section:'Billing', items:[
    {id:'billing',   label:'Billing & Costs', icon:'billing'},
    {id:'mykeys',    label:'My Provider Keys',icon:'key'},
  ]},
  {section:'Developer', items:[
    {id:'apikeys',   label:'API Keys',        icon:'apikey'},
    {id:'playground',label:'Playground',      icon:'playground'},
    {id:'explorer',  label:'Routing Explorer',icon:'route'},
    {id:'orgrouting',label:'Routing Rules',   icon:'filter'},
    {id:'evals',     label:'Evals',           icon:'checkcircle'},
    {id:'a2a',       label:'A2A Agents',      icon:'route'},
    {id:'logs',      label:'Request Logs',    icon:'search'},
  ]},
  {section:'Team', items:[
    {id:'team',      label:'Team Members',    icon:'team'},
    {id:'settings',  label:'Settings',        icon:'settings'},
  ]},
  {section:'Docs', items:[
    {id:'docs',      label:'Documentation',   icon:'book'},
  ]},
];
const ADMIN_NAV = {section:'Admin · SISL', admin:true, items:[
  {id:'customers',  label:'Customers',         icon:'customers'},
  {id:'analytics',  label:'Analytics',         icon:'analytics'},
  {id:'sislkeys',   label:'Provider Keys',     icon:'provider'},
  {id:'pricing',    label:'Pricing',           icon:'pricing'},
  {id:'modelpref',  label:'Model Performance', icon:'modelpref'},
  {id:'lifecycle',  label:'Model Lifecycle',   icon:'layers'},
  {id:'routing',    label:'Routing Rules',     icon:'route'},
  {id:'pricebook',  label:'Price Book',        icon:'billing'},
  {id:'health',     label:'Health Monitor',    icon:'health'},
  {id:'access',     label:'Login Access',      icon:'shield'},
  {id:'audit',      label:'Audit Log',         icon:'audit'},
]};

const PAGE_META = {};
[...NAV, ADMIN_NAV].forEach(s=>s.items.forEach(it=>PAGE_META[it.id]={title:it.label, section:s.section}));

const PUBLIC = ['landing','login','onboarding','contact-sales','resources','privacy','terms','code'];

// ROUTE_ACCESS maps each customer route to the org roles allowed to reach it,
// per docs/role-matrix.md. This is the SPA half of role enforcement (the
// backend RequireRole gates are the real control); without it every
// authenticated user saw the full org-admin nav — an org_member looked like
// an admin. Team management + org Settings are org_admin-only; Billing & BYOK
// keys are org_admin/org_viewer/org_billing; everything else is the shared
// member/viewer working set. ("developer" = the legacy default key role,
// treated as org_member.)
const ROUTE_ACCESS = {
  dashboard:  ['org_admin','org_member','org_viewer','developer'],
  usage:      ['org_admin','org_member','org_viewer','developer'],
  billing:    ['org_admin','org_viewer','org_billing'],
  mykeys:     ['org_admin','org_viewer','org_billing'],
  apikeys:    ['org_admin','org_member','org_viewer','developer'],
  playground: ['org_admin','org_member','org_viewer','developer'],
  explorer:   ['org_admin','org_member','org_viewer','developer'],
  evals:      ['org_admin','org_member','org_viewer','developer'],
  // Mutations (create/update/delete agent) are org_admin-only server-side
  // (internal/gateway/a2a_endpoints.go's mutateAccess); reads are broader.
  // The page itself only exposes the New/Edit/Delete affordances — org_admin
  // is the appropriate nav-level gate, matching Team/Settings below.
  a2a:        ['org_admin'],
  // Mutations are org_admin-only server-side (and entitlement-gated); the page
  // exposes the editors, so gate the nav like Team/Settings/A2A.
  orgrouting: ['org_admin'],
  logs:       ['org_admin','org_member','org_viewer','developer'],
  team:       ['org_admin'],
  settings:   ['org_admin'],
  // Read-only reference documentation — every role that can reach the
  // dashboard at all should be able to read it, including org_billing.
  docs:       ['org_admin','org_member','org_viewer','org_billing','developer'],
};
// allowedRoutesFor returns null when the user may reach EVERY route (SISL
// admin), else a {routeId:true} allow-set from ROUTE_ACCESS ∪ the roles.
function allowedRoutesFor(roles){
  roles = roles || [];
  if(roles.some(function(r){return r==='admin'||r==='sisl_admin';})) return null;
  var set={};
  Object.keys(ROUTE_ACCESS).forEach(function(id){
    if(ROUTE_ACCESS[id].some(function(r){return roles.indexOf(r)>=0;})) set[id]=true;
  });
  // Fallback so an unrecognized-role user is never locked out entirely.
  if(Object.keys(set).length===0){ set={dashboard:true,usage:true,playground:true,apikeys:true,explorer:true,logs:true}; }
  return set;
}
// firstAllowedRoute picks a sensible landing/redirect target from an allow-set.
function firstAllowedRoute(set){
  if(!set) return 'dashboard';
  var order=['dashboard','usage','playground','apikeys','explorer','logs','billing','mykeys','team','settings'];
  for(var i=0;i<order.length;i++){ if(set[order[i]]) return order[i]; }
  return 'dashboard';
}

/* ---------- Contact Sales (shown when a login domain isn't whitelisted) ---------- */
function ContactSalesPage(){
  return (
    <div style={{minHeight:'100vh',display:'flex',alignItems:'center',justifyContent:'center',background:'var(--bg-0)',padding:24}}>
      <div className="card card-pad" style={{maxWidth:480,textAlign:'center',padding:'40px 32px'}}>
        <div style={{display:'flex',justifyContent:'center',marginBottom:18}}><LogoMark size={48}/></div>
        <h1 style={{fontFamily:'var(--font-display)',fontSize:24,margin:'0 0 10px'}}>Access not enabled yet</h1>
        <p className="faint" style={{lineHeight:1.6,fontSize:14.5,margin:'0 0 8px'}}>
          Your organization isn't provisioned on NeuroRoute yet. Access is granted by the
          <b style={{color:'var(--tx-1)'}}> SISL CloudWorx</b> team on a per-organization basis.
        </p>
        <p className="faint" style={{lineHeight:1.6,fontSize:14.5,margin:'0 0 24px'}}>
          Get in touch to have your domain enabled and start routing your AI workloads.
        </p>
        <div className="grid" style={{gap:10,maxWidth:320,margin:'0 auto'}}>
          <a className="btn btn-grad btn-block" href="mailto:raman@sislinfotech.com?subject=NeuroRoute%20access%20request">
            <Icon name="send" size={15}/>Contact SISL CloudWorx Sales
          </a>
          <button className="btn btn-ghost btn-block" onClick={function(){go('login');}}>Back to sign in</button>
        </div>
        <div className="faint" style={{fontSize:12,marginTop:22}}>raman@sislinfotech.com · Sisl Infotech</div>
      </div>
    </div>
  );
}
window.PAGES = Object.assign(window.PAGES||{}, {'contact-sales':ContactSalesPage});

function parseHash(){
  const h=(location.hash||'').replace(/^#\/?/,'').split('?')[0];
  return h||'landing';
}
function go(route){ location.hash='#/'+route; }
window.go = go;

/* ---------- Error boundary ---------- */
class ErrorBoundary extends React.Component {
  constructor(props){ super(props); this.state={error:null}; }
  static getDerivedStateFromError(error){ return {error: error}; }
  componentDidCatch(error, info){ console.error('[NeuroRoute] page error:', error, info); }
  render(){
    if(this.state.error){
      const msg = (this.state.error && this.state.error.message) || String(this.state.error);
      const stack = (this.state.error && this.state.error.stack) || '';
      return (
        <div className="error-boundary">
          <Icon name="alert" size={36} style={{color:'var(--red)'}}/>
          <h2>Something broke on this page</h2>
          <p className="faint">An unexpected error occurred. The rest of the dashboard still works.</p>
          <div className="err-trace">{msg}{stack ? '\n\n'+stack : ''}</div>
          <div className="row" style={{gap:10,justifyContent:'center',marginTop:18}}>
            <button className="btn btn-primary" onClick={()=>{this.setState({error:null}); go('dashboard');}}>Back to Dashboard</button>
            <button className="btn btn-ghost" onClick={()=>location.reload()}>Reload</button>
          </div>
        </div>
      );
    }
    return this.props.children;
  }
}

/* ---------- Sidebar ---------- */
function Sidebar({route, adminMode, allowed, open, onClose}){
  // allowed null = full access (SISL admin gets the admin nav too); otherwise
  // filter each section's items to the role's allow-set and drop empty sections.
  const sections = allowed
    ? NAV.map(sec=>({...sec, items: sec.items.filter(it=>allowed[it.id])})).filter(sec=>sec.items.length>0)
    : (adminMode ? [...NAV, ADMIN_NAV] : NAV);
  const canSettings = !allowed || allowed['settings'];
  const canApiKeys = !allowed || allowed['apikeys'];
  const [menuOpen, setMenuOpen] = React.useState(false);
  const userRef = React.useRef(null);

  /* Close menu on outside-click */
  React.useEffect(()=>{
    if(!menuOpen) return;
    const onDoc = (e)=>{ if(userRef.current && !userRef.current.contains(e.target)) setMenuOpen(false); };
    document.addEventListener('mousedown', onDoc);
    return ()=>document.removeEventListener('mousedown', onDoc);
  }, [menuOpen]);

  const logout = ()=>{
    if(window.NR_API && window.NR_API.logout){
      window.NR_API.logout().then(()=>{ go('login'); }).catch(()=>{ go('login'); });
    } else { go('landing'); }
  };

  return (
    <aside className={'sidebar'+(open?' open':'')}>
      <div className="sb-brand">
        <LogoMark size={32}/>
        <div className="stack" style={{marginLeft:10}}>
          <span style={{fontFamily:'var(--font-display)',fontWeight:700,fontSize:16,background:'var(--grad-wordmark)',WebkitBackgroundClip:'text',backgroundClip:'text',color:'transparent',letterSpacing:'-0.01em'}}>NeuroRoute</span>
          <span className="faint" style={{fontSize:10,letterSpacing:'.08em',textTransform:'uppercase'}}>AI Cost Router</span>
        </div>
      </div>
      <nav className="sb-nav">
        {sections.map(sec=>(
          <div key={sec.section}>
            <div className="sb-section" style={sec.admin?{color:'var(--amber)'}:null}>{sec.section}</div>
            {sec.items.map(it=>(
              <div key={it.id} className={'sb-link'+(route===it.id?' active':'')}
                onClick={()=>{go(it.id);onClose&&onClose();}}>
                <Icon name={it.icon} className="ico"/>
                <span>{it.label}</span>
                {sec.admin && <Icon name="shield" size={12} style={{marginLeft:'auto',color:'var(--tx-3)'}}/>}
              </div>
            ))}
          </div>
        ))}
      </nav>
      <div className="sb-foot" style={{position:'relative'}} ref={userRef}>
        {menuOpen && <div className="user-menu">
          {canSettings && <div className="user-menu-item" onClick={()=>{setMenuOpen(false); go('settings');}}>
            <Icon name="settings" size={15}/>Settings
          </div>}
          {canApiKeys && <div className="user-menu-item" onClick={()=>{setMenuOpen(false); go('apikeys');}}>
            <Icon name="apikey" size={15}/>API Keys
          </div>}
          <div style={{height:1,background:'var(--line)',margin:'4px 0'}}/>
          <div className="user-menu-item danger" onClick={logout}>
            <Icon name="logout" size={15}/>Sign out
          </div>
        </div>}
        <div className="sb-user" onClick={()=>setMenuOpen(o=>!o)} title="Open user menu">
          <div className="avatar" style={{background:DATA.USER.color}}>{DATA.USER.initials}</div>
          <div className="stack" style={{flex:1,minWidth:0}}>
            <span style={{fontSize:13,fontWeight:700,whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>{DATA.USER.name||'User'}</span>
            <span className="faint" style={{fontSize:11,whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>{DATA.USER.email||DATA.ORG.name}</span>
          </div>
          <Icon name="chevdown" size={14} style={{color:'var(--tx-2)'}}/>
        </div>
      </div>
    </aside>
  );
}

/* ---------- Topbar ---------- */
function Topbar({route, adminMode, onMenu}){
  const meta=PAGE_META[route]||{title:'Dashboard',section:'Overview'};
  return (
    <header className="topbar">
      <button className="iconbtn hamburger" onClick={onMenu}><Icon name="menu" size={18}/></button>
      <div style={{flex:1,minWidth:0}}>
        <div className="crumb">{meta.section} <span style={{opacity:.4}}>/</span> {meta.title}</div>
        <h1>{meta.title}</h1>
      </div>
      {adminMode && <Badge color="amber" style={{fontSize:11}}>Admin</Badge>}
    </header>
  );
}

/* ---------- App ---------- */
function App(){
  const [route,setRoute]=useState(parseHash());
  const [adminMode,setAdminMode]=useState(false);
  // allowed: null = every route (SISL admin or not yet resolved); otherwise a
  // {routeId:true} allow-set from the user's org role (see allowedRoutesFor).
  const [allowed,setAllowed]=useState(null);
  const [navOpen,setNavOpen]=useState(false);
  const [authState,setAuthState]=useState('checking'); // 'checking' | 'authenticated' | 'anonymous'
  // The live model catalog (GET /v1/models/catalog) replaces data.js's bundled
  // model table, which knew only the compiled-in fleet — so an admin-activated
  // DB-catalog model showed up as provider "unknown" at $0.00/quality 0 and was
  // missing from every model picker. Gating the authenticated render on this
  // (rather than letting pages mount and patching them afterwards) means a page
  // never reads a half-hydrated table, so there's no flicker and no remount.
  // Resolves either way — a catalog outage leaves the bundled fallback standing.
  const [catalogReady,setCatalogReady]=useState(false);

  // Hydrate the model catalog on mount, in parallel with the auth check —
  // it's an authenticated endpoint, but the cookie is already present on a
  // returning session, and an anonymous caller just gets the fallback.
  useEffect(()=>{
    if(window.DATA && window.DATA.hydrateCatalog){
      window.DATA.hydrateCatalog().then(function(){ setCatalogReady(true); });
    } else {
      setCatalogReady(true);
    }
  },[]);

  // Check authentication on mount.
  useEffect(()=>{
    if(window.NR_API && window.NR_API.me){
      window.NR_API.me().then(data=>{
        if(data && (data.key_id || data.email)){
          setAuthState('authenticated');
          // Update DATA.USER and DATA.ORG with real auth data.
          if(data.name) DATA.USER.name = data.name;
          if(data.email) DATA.USER.email = data.email;
          if(data.name) DATA.USER.initials = data.name.split(' ').map(w=>w[0]).join('').toUpperCase().slice(0,2);
          else if(data.email) DATA.USER.initials = data.email.slice(0,2).toUpperCase();
          if(data.org_name) DATA.ORG.name = data.org_name;
          if(data.org_id) DATA.ORG.id = data.org_id;
          // Role-based access: SISL admins get the admin nav + every route;
          // every other role is gated to its allowed route set (this is what
          // stops an org_member from seeing/reaching the org-admin surfaces —
          // Team, Settings, Billing — that made a member look like an admin).
          const roles = data.roles || [];
          const isSISL = roles.indexOf('admin')>=0 || roles.indexOf('sisl_admin')>=0;
          if(isSISL) setAdminMode(true);
          // Self-scoped: an org_member (or role-less legacy user) sees only their
          // own usage/keys — the backend enforces this on every data endpoint;
          // this flag lets pages show a "your own data only" cue. Anyone with an
          // org-wide or SISL role sees everything, so they are NOT self-scoped.
          const orgWide = ['org_admin','org_viewer','org_billing','admin','sisl_admin','sisl_developer'];
          window.NR_SELF_SCOPED = !roles.some(function(r){return orgWide.indexOf(r)>=0;});
          const allow = allowedRoutesFor(roles); // null for SISL
          setAllowed(allow);
          // Bounce a landing on a route the role can't reach to its home page.
          if(allow){
            const cur = parseHash();
            if(!PUBLIC.includes(cur) && !allow[cur]) go(firstAllowedRoute(allow));
          }
        } else {
          setAuthState('anonymous');
        }
      }).catch(()=>setAuthState('anonymous'));
    } else {
      setAuthState('anonymous');
    }
  },[]);

  useEffect(()=>{
    const h=()=>{ setRoute(parseHash()); setNavOpen(false); window.scrollTo(0,0);
      const m=document.querySelector('.main'); if(m)m.scrollTop=0; };
    window.addEventListener('hashchange',h); return ()=>window.removeEventListener('hashchange',h);
  },[]);

  // When any authenticated request 401s (session expired, 15-min TTL), log the
  // user out cleanly and send them to sign-in — instead of leaving the page
  // showing blanked-out numbers until a manual refresh.
  useEffect(()=>{
    window.__nrSessionExpired = function(){
      if(window.__nrExpiring) return; window.__nrExpiring = true;
      setAuthState('anonymous');
      go('login');
      setTimeout(function(){ window.__nrExpiring = false; }, 4000);
    };
    return function(){ window.__nrSessionExpired = null; };
  },[]);

  const PAGES = window.PAGES||{};
  const Page = PAGES[route] || PAGES['dashboard'];

  // Public full-screen pages (always accessible).
  if(PUBLIC.includes(route)){
    const P = PAGES[route];
    return P ? <ErrorBoundary><P go={go}/></ErrorBoundary> : <div style={{padding:40}}>Loading…</div>;
  }

  // Protected pages: redirect to login if not authenticated.
  // Waiting on catalogReady too, so no page mounts against a half-hydrated
  // model table (see the catalogReady declaration above).
  if(authState==='checking' || (authState==='authenticated' && !catalogReady)){
    return <div style={{display:'flex',alignItems:'center',justifyContent:'center',height:'100vh',background:'var(--bg-0)'}}><LoadingPage/></div>;
  }
  if(authState==='anonymous'){
    if(route!=='landing' && route!=='login'){
      go('login');
      return null;
    }
  }
  // Role guard: any direct/manual navigation (hash edit, bookmark) to a route
  // the role can't reach bounces to its home page — the sidebar already only
  // renders allowed links, this covers the rest. `allowed` null = full access.
  if(allowed && !allowed[route]){
    go(firstAllowedRoute(allowed));
    return null;
  }

  return (
    <div className="app">
      {navOpen && <div className="sb-backdrop" onClick={()=>setNavOpen(false)}/>}
      <Sidebar route={route} adminMode={adminMode} allowed={allowed} open={navOpen} onClose={()=>setNavOpen(false)}/>
      <div className="main">
        <Topbar route={route} adminMode={adminMode} onMenu={()=>setNavOpen(true)}/>
        <div className="page">
          <ErrorBoundary key={route}>
            {Page ? <Page go={go} setAdminMode={setAdminMode}/> : <div className="empty"><div className="e-ico"><Icon name="layers" size={26}/></div><h3>Coming soon</h3></div>}
          </ErrorBoundary>
        </div>
      </div>
    </div>
  );
}

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