/* ============================================================
   NeuroRoute — shared UI primitives + chart wrappers (Chart.js)
   ============================================================ */
const {useState, useEffect, useRef, useMemo, useCallback, createContext, useContext} = React;

const CSSV = (n)=>getComputedStyle(document.documentElement).getPropertyValue(n).trim();
const COL = {blue:'#3b82f6',cyan:'#22d3ee',magenta:'#e24fd6',amber:'#f59e0b',green:'#10b981',
  purple:'#8b5cf6',pink:'#ec4899',red:'#f43f5e',tx2:'#727b91',line:'rgba(255,255,255,.07)'};

/* ---------- toasts ---------- */
const ToastCtx = createContext(()=>{});
const useToast = ()=>useContext(ToastCtx);
function ToastHost({children}){
  const [toasts,setToasts]=useState([]);
  const push = useCallback((t)=>{
    const id=Math.random().toString(36).slice(2);
    setToasts(x=>[...x,{id,kind:'success',...t}]);
    setTimeout(()=>setToasts(x=>x.filter(y=>y.id!==id)), t.duration||3600);
  },[]);
  const map={success:['check','b-green'],error:['x','b-red'],info:['info','b-blue'],warn:['alert','b-amber']};
  return (
    <ToastCtx.Provider value={push}>
      {children}
      <div className="toast-wrap">
        {toasts.map(t=>{
          const [ico,cls]=map[t.kind]||map.success;
          return (
            <div className="toast" key={t.id}>
              <div className={'t-ico badge '+cls} style={{borderRadius:8}}><Icon name={ico} size={16}/></div>
              <div style={{flex:1}}>
                <div className="t-title">{t.title}</div>
                {t.msg && <div className="t-msg">{t.msg}</div>}
              </div>
              <button className="iconbtn" style={{width:24,height:24}} onClick={()=>setToasts(x=>x.filter(y=>y.id!==t.id))}><Icon name="x" size={13}/></button>
            </div>
          );
        })}
      </div>
    </ToastCtx.Provider>
  );
}

/* ---------- modal ---------- */
function Modal({title, sub, onClose, children, footer, lg}){
  useEffect(()=>{
    const h=(e)=>{if(e.key==='Escape')onClose();};
    window.addEventListener('keydown',h); return ()=>window.removeEventListener('keydown',h);
  },[onClose]);
  return (
    <div className="modal-overlay" onClick={onClose}>
      <div className={'modal'+(lg?' modal-lg':'')} onClick={e=>e.stopPropagation()}>
        <div className="modal-hd">
          <div><div className="card-title">{title}</div>{sub&&<div className="card-sub">{sub}</div>}</div>
          <button className="iconbtn" onClick={onClose}><Icon name="x" size={15}/></button>
        </div>
        <div className="modal-bd">{children}</div>
        {footer && <div className="modal-ft">{footer}</div>}
      </div>
    </div>
  );
}

/* ---------- small bits ---------- */
function Badge({color='gray', dot, children, style}){
  return <span className={'badge b-'+color} style={style}>{dot&&<span className="dot"/>}{children}</span>;
}
function TaskBadge({task}){ return <Badge color={task.color}>{task.label}</Badge>; }

function Switch({on, onChange}){
  return <div className={'switch'+(on?' on':'')} onClick={()=>onChange(!on)} role="switch" aria-checked={on}/>;
}

function Field({label, hint, children}){
  return <label className="field">{label&&<span className="label">{label}</span>}{children}{hint&&<span className="hint">{hint}</span>}</label>;
}

function CopyBtn({text, label='Copy'}){
  const t=useToast(); const [done,setDone]=useState(false);
  return (
    <button className="btn btn-soft btn-sm" onClick={()=>{
      navigator.clipboard?.writeText(text); setDone(true); t({title:'Copied to clipboard'});
      setTimeout(()=>setDone(false),1400);
    }}><Icon name={done?'check':'copy'} size={14}/>{done?'Copied':label}</button>
  );
}

function ProviderLogo({id, size}){
  const p=DATA.providerById(id)||{abbr:'?',color:'#888'};
  return <div className="plogo" style={{background:p.color+'22',color:p.color,
    width:size,height:size,borderColor:p.color+'44'}}>{p.abbr}</div>;
}

function Metric({icon, color='blue', value, label, delta, deltaUp, sub}){
  return (
    <div className="card metric">
      <div className="m-top">
        <div className="m-ico" style={{background:`var(--${color}-t)`,color:`var(--${color})`}}><Icon name={icon} size={19}/></div>
        {delta!=null && <span className="m-delta" style={{color: deltaUp?'var(--green)':'var(--red)'}}>
          <Icon name={deltaUp?'trendup':'trenddown'} size={13}/>{delta}</span>}
      </div>
      <div className="m-val tnum">{value}</div>
      <div className="m-label">{label}{sub&&<span style={{color:'var(--tx-3)'}}> · {sub}</span>}</div>
    </div>
  );
}

/* ============================================================
   CHARTS (Chart.js wrappers)
   ============================================================ */
function useChart(cfgFactory, deps){
  const ref=useRef(null); const inst=useRef(null);
  useEffect(()=>{
    if(!ref.current||!window.Chart) return;
    const cfg=cfgFactory();
    inst.current=new Chart(ref.current.getContext('2d'), cfg);
    return ()=>inst.current && inst.current.destroy();
  }, deps);
  return ref;
}
const gridCfg = {color:COL.line, drawTicks:false};
const tickCfg = {color:COL.tx2, font:{family:'JetBrains Mono', size:10.5}, padding:6};
const tooltipCfg = {
  backgroundColor:'#171b29', borderColor:'rgba(255,255,255,.12)', borderWidth:1,
  titleColor:'#eef1f8', bodyColor:'#aab2c5', padding:11, cornerRadius:9, displayColors:true,
  boxPadding:4, titleFont:{family:'Manrope',weight:'700'}, bodyFont:{family:'JetBrains Mono',size:11.5},
};

function LineDual({labels, a, b, height=260}){
  const ref=useChart(()=>{
    const mk=(ctx,c)=>{const g=ctx.createLinearGradient(0,0,0,height);g.addColorStop(0,c+'55');g.addColorStop(1,c+'00');return g;};
    return {type:'line', data:{labels, datasets:[
      {label:'Claude Fable 5 baseline', data:b, borderColor:COL.red, borderWidth:2, borderDash:[5,4],
        pointRadius:0, tension:.35, fill:false},
      {label:'With NeuroRoute', data:a, borderColor:COL.blue, borderWidth:2.5, pointRadius:0,
        tension:.35, fill:true, backgroundColor:(c)=>mk(c.chart.ctx,COL.blue)},
    ]},
    options:{responsive:true,maintainAspectRatio:false,interaction:{mode:'index',intersect:false},
      plugins:{legend:{display:false}, tooltip:{...tooltipCfg, callbacks:{label:(c)=>' '+c.dataset.label+': $'+c.raw.toLocaleString()}}},
      scales:{x:{grid:{display:false},ticks:{...tickCfg,maxTicksLimit:8}},
        y:{grid:gridCfg,border:{display:false},ticks:{...tickCfg,callback:v=>'$'+v}}}}};
  },[labels.join(),a.join(),b.join()]);
  return <div style={{height}}><canvas ref={ref}/></div>;
}

function BarStacked({labels, series, height=260}){
  const ref=useChart(()=>({
    type:'bar', data:{labels, datasets:series.map(s=>({label:s.label,data:s.data,backgroundColor:s.color,
      borderRadius:4, borderSkipped:false, barPercentage:.7, categoryPercentage:.78, stack:'a'}))},
    options:{responsive:true,maintainAspectRatio:false,interaction:{mode:'index',intersect:false},
      plugins:{legend:{display:false},tooltip:{...tooltipCfg,callbacks:{label:c=>' '+c.dataset.label+': '+fmtTok(c.raw)}}},
      scales:{x:{stacked:true,grid:{display:false},ticks:{...tickCfg,maxTicksLimit:10}},
        y:{stacked:true,grid:gridCfg,border:{display:false},ticks:{...tickCfg,callback:v=>fmtTok(v)}}}}}),
  [labels.join(),series.map(s=>s.data.join()).join()]);
  return <div style={{height}}><canvas ref={ref}/></div>;
}

function BarSimple({labels, data, color=COL.blue, height=240, fmt}){
  const ref=useChart(()=>({
    type:'bar', data:{labels, datasets:[{data, backgroundColor:color+'cc', hoverBackgroundColor:color,
      borderRadius:5, borderSkipped:false, barPercentage:.66, categoryPercentage:.8}]},
    options:{responsive:true,maintainAspectRatio:false,
      plugins:{legend:{display:false},tooltip:{...tooltipCfg,callbacks:{label:c=>' '+(fmt?fmt(c.raw):c.raw.toLocaleString())}}},
      scales:{x:{grid:{display:false},ticks:{...tickCfg,maxTicksLimit:12}},
        y:{grid:gridCfg,border:{display:false},ticks:{...tickCfg,callback:v=>fmt?fmt(v):v.toLocaleString()}}}}}),
  [labels.join(),data.join()]);
  return <div style={{height}}><canvas ref={ref}/></div>;
}

function Donut({items, height=230, centerLabel, centerSub}){
  const palette=[COL.cyan,COL.blue,COL.magenta,COL.green,COL.purple,COL.amber,COL.pink];
  const ref=useChart(()=>({
    type:'doughnut', data:{labels:items.map(i=>i.label), datasets:[{data:items.map(i=>i.value),
      backgroundColor:items.map((i,k)=>i.color||palette[k%palette.length]), borderColor:'#11141f', borderWidth:3, hoverOffset:6}]},
    options:{responsive:true,maintainAspectRatio:false,cutout:'68%',
      plugins:{legend:{display:false},tooltip:{...tooltipCfg,callbacks:{label:c=>' '+c.label+': '+c.raw+'%'}}}}}),
  [items.map(i=>i.value).join()]);
  return (
    <div style={{height,position:'relative'}}>
      <canvas ref={ref}/>
      {centerLabel!=null&&<div style={{position:'absolute',inset:0,display:'grid',placeItems:'center',pointerEvents:'none'}}>
        <div style={{textAlign:'center'}}>
          <div className="display tnum" style={{fontSize:26}}>{centerLabel}</div>
          <div className="faint" style={{fontSize:11.5}}>{centerSub}</div>
        </div></div>}
    </div>
  );
}

/* The ring is an SVG with a viewBox, so it scales cleanly with `size`. The
   label is HTML and does NOT — a fixed font (styles.css sets 21px, sized for
   the 96px default) spills outside the ring on the smaller gauges, which is
   what the Health cards use. So compute the font size here.

   The largest font that fits: available width / (chars * glyph width). Space
   Grotesk 600 measures 0.639em per digit, and the usable chord inside the ring
   is ~0.68*size (the full inner diameter is (96-7)/96 of it, less the bit lost
   to the circle curving away at the text's own height, less ~2.5px per side so
   the text doesn't crowd the stroke).
   Capped at the design's 21px so a full-size gauge renders exactly as before.

   Width is budgeted for at least 4 characters even when the value is shorter,
   so every gauge in a grid uses ONE font size — otherwise "30%" would render
   noticeably larger than "100%" in the card right next to it. */
function gaugeFontSize(size, text){
  const chars = Math.max(text.length, 4);
  return Math.min(21, (0.68*size)/(chars*0.639));
}

function Gauge({value, size=96, color, label}){
  const c = color || (value>=98?COL.green:value>=95?COL.cyan:value>=90?COL.amber:COL.red);
  const R=42, C=2*Math.PI*R, off=C*(1-value/100);
  const txt = String(value)+(label||'%');
  return (
    <div className="gauge" style={{width:size,height:size}}>
      <svg width={size} height={size} viewBox="0 0 96 96">
        <circle cx="48" cy="48" r={R} fill="none" stroke="rgba(255,255,255,.07)" strokeWidth="7"/>
        <circle cx="48" cy="48" r={R} fill="none" stroke={c} strokeWidth="7" strokeLinecap="round"
          strokeDasharray={C} strokeDashoffset={off} style={{transition:'stroke-dashoffset 1s ease'}}/>
      </svg>
      <div className="g-val" style={{color:c,fontSize:gaugeFontSize(size,txt).toFixed(2)+'px'}}>{txt}</div>
    </div>
  );
}

/* tiny inline SVG sparkline */
function Spark({data, color=COL.green, w=120, h=34, fill=true}){
  const max=Math.max(...data), min=Math.min(...data), rng=(max-min)||1;
  const pts=data.map((v,i)=>[i/(data.length-1)*w, h-4-((v-min)/rng)*(h-8)]);
  const d=pts.map((p,i)=>(i?'L':'M')+p[0].toFixed(1)+' '+p[1].toFixed(1)).join(' ');
  const id='sp'+useMemo(()=>Math.random().toString(36).slice(2,7),[]);
  return (
    <svg className="spark" width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
      <defs><linearGradient id={id} x1="0" y1="0" x2="0" y2="1">
        <stop offset="0" stopColor={color} stopOpacity=".35"/><stop offset="1" stopColor={color} stopOpacity="0"/></linearGradient></defs>
      {fill&&<path d={d+` L${w} ${h} L0 ${h} Z`} fill={`url(#${id})`}/>}
      <path d={d} fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
    </svg>
  );
}

/* shared page header: context line + actions (title is in topbar) */
function PageHead({desc, children}){
  return (
    <div className="spread" style={{marginBottom:22,flexWrap:'wrap',gap:12}}>
      <p style={{color:'var(--tx-2)',fontSize:14,margin:0,maxWidth:620}}>{desc}</p>
      {children && <div className="row" style={{gap:10}}>{children}</div>}
    </div>
  );
}
/* horizontal proportion bar used in tables */
function MiniBar({pct, color=COL.blue}){
  return <div className="meter" style={{width:90,display:'inline-block',verticalAlign:'middle'}}><span style={{width:pct+'%',background:color}}/></div>;
}
function fmtUSD(n,d=2){return '$'+n.toLocaleString('en-US',{minimumFractionDigits:d,maximumFractionDigits:d});}
function fmtNum(n){return n.toLocaleString('en-US');}
function fmtTok(n){return n>=1e6?(n/1e6).toFixed(2)+'M':n>=1e3?(n/1e3).toFixed(1)+'K':n;}

/* ── Loading / Empty states ─────────────────────────────── */
function LoadingPage(){
  return (
    <div style={{display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',minHeight:400,gap:16}}>
      <div style={{width:40,height:40,borderRadius:'50%',border:'3px solid var(--line)',borderTopColor:'var(--blue)',animation:'spin .8s linear infinite'}}/>
      <span className="faint" style={{fontSize:13}}>Loading…</span>
      <style>{`@keyframes spin{to{transform:rotate(360deg)}}`}</style>
    </div>
  );
}

function EmptyState({icon, title, desc, actions}){
  return (
    <div className="card" style={{padding:'60px 40px',textAlign:'center'}}>
      <div style={{width:56,height:56,borderRadius:16,background:'var(--bg-inset)',display:'flex',alignItems:'center',justifyContent:'center',margin:'0 auto 16px'}}>
        <Icon name={icon||'layers'} size={24} style={{color:'var(--tx-2)'}}/>
      </div>
      <h3 style={{margin:'0 0 8px',fontSize:18}}>{title}</h3>
      {desc && <p className="faint" style={{margin:'0 0 20px',maxWidth:440,marginLeft:'auto',marginRight:'auto',lineHeight:1.6}}>{desc}</p>}
      {actions && actions.length > 0 && <div className="row" style={{gap:10,justifyContent:'center'}}>
        {actions.map((a,i)=><button key={i} className={a.primary?'btn btn-primary btn-sm':'btn btn-ghost btn-sm'} onClick={a.onClick}>{a.label}</button>)}
      </div>}
    </div>
  );
}

/* ── Health sidebar component ──────────────────────────── */
function HealthSidebar({healthData}){
  const providers = groupHealthByProvider(healthData);
  if(providers.length===0) return (
    <SectionCard title="Provider health" sub="No providers connected">
      <div className="faint" style={{padding:20,textAlign:'center'}}>No provider data available.</div>
    </SectionCard>
  );
  return (
    <SectionCard title="Provider health" sub="Live status across providers" pad={false}>
      <div style={{padding:'6px 0'}}>
        {providers.map(h=>{
          const c = h.status==='optimal'?COL.green:h.status==='degraded'?COL.amber:COL.red;
          return (
            <div key={h.id} className="spread" style={{padding:'11px 20px',borderBottom:'1px solid var(--line)'}}>
              <span className="row" style={{gap:10}}>
                <span className="statusdot pulse" style={{background:c,color:c}}/>
                <ProviderLogo id={h.id} size={26}/>
                <span style={{fontWeight:600}}>{h.name}</span>
              </span>
              <span className="row" style={{gap:8}}>
                <span className="mono" style={{fontSize:12,color:c}}>{h.healthy}/{h.total}</span>
              </span>
            </div>
          );
        })}
      </div>
    </SectionCard>
  );
}

function SectionCard({title, sub, action, children, pad=true}){
  return (
    <div className="card">
      <div className="card-hd"><div><div className="card-title">{title}</div>{sub&&<div className="card-sub">{sub}</div>}</div>{action}</div>
      <div style={pad?{padding:20}:null}>{children}</div>
    </div>
  );
}

/* ---------- alerts (1.4) — shared between customer Settings and admin edit-org ---------- */
var ALERT_RULE_TYPES=[
  {value:'budget_pct', label:'Budget % threshold', unit:'%', hint:"Fires when day or month spend crosses this % of a configured cap."},
  {value:'error_rate', label:'Error rate', unit:'%', hint:'Fires when the error rate over the last 15 minutes crosses this %.'},
  {value:'latency_p95', label:'Latency p95', unit:'ms', hint:'Fires when p95 request latency over the last 15 minutes crosses this many ms.'},
  {value:'spend_anomaly', label:'Spend anomaly', unit:'x', hint:"Fires when today's spend is this many times the trailing 7-day daily average."},
  {value:'provider_health', label:'Provider health flip', unit:'', hint:'Fires when any provider currently shows unhealthy.'},
];
function alertTypeDef(v){ return ALERT_RULE_TYPES.filter(function(d){return d.value===v;})[0] || ALERT_RULE_TYPES[0]; }

/* ---------- guardrails (2.5) — shared between admin.jsx (Customers -> Edit)
   and portal-team.jsx (Settings). Was previously duplicated byte-for-byte in
   both files; consolidated here following the ALERT_RULE_TYPES pattern above
   so a rule added to the backend registry only needs to be added in one
   place. NOTE: this list MUST mirror the backend registry — see
   internal/guardrails/engine.go Registry() (input-phase rules) and
   internal/guardrails/output.go OutputRegistry() (output-phase rules). If a
   rule exists on the backend but is missing here, buildGuardrailsConfig's
   preserve-unknown handling keeps it from being destroyed on save, but it
   still won't be editable from this UI — add it here too. */
var GUARDRAIL_RULE_DEFS=[
  {name:'pii', label:'PII detection', desc:'Detects Aadhaar/PAN/email/phone/card/SSN in user messages.', defaultEnabled:true, defaultAction:'flag', phase:'input'},
  {name:'secret-detector', label:'Secret detector', desc:'Detects AWS/OpenAI-style/GitHub keys and PEM private-key blocks.', defaultEnabled:true, defaultAction:'block', phase:'input'},
  {name:'length-cap', label:'Length cap', desc:'Caps combined user-message length.', defaultEnabled:true, defaultAction:'block', phase:'input'},
  {name:'denylist', label:'Custom denylist', desc:'Block/flag on custom regex patterns (one per line). Off by default — no platform pattern set.', defaultEnabled:false, defaultAction:'block', phase:'input'},
  {name:'prompt-injection', label:'Prompt-injection detection', desc:'Heuristic scan for instruction-override phrasing, jailbreak/role-hijack attempts, model-format template tokens, and hidden Unicode text in user messages.', defaultEnabled:true, defaultAction:'flag', phase:'input'},
  {name:'moderation', label:'Content moderation (LLM)', desc:'Routes the message to a cheap model for a binary policy-violation classification (harmful/abusive/illegal content). Off by default — adds a sub-request per message.', defaultEnabled:false, defaultAction:'block', phase:'input'},
  {name:'vision', label:'Image validation', desc:'Rejects image attachments with a disallowed MIME type or an oversized base64 payload (default: standard image types, 10 MB).', defaultEnabled:false, defaultAction:'flag', phase:'input'},
  {name:'json-schema', label:'JSON schema validation', desc:'Checks the response is valid JSON with the required top-level keys (comma-separated). Auto-retries the same model once on failure, then always lets the response through.', defaultEnabled:false, defaultAction:'block', phase:'output'},
  {name:'output-denylist', label:'Output denylist', desc:'Block/flag/redact responses matching custom regex patterns (one per line).', defaultEnabled:false, defaultAction:'flag', phase:'output'},
  {name:'output-length-cap', label:'Output length cap', desc:'Caps the model response length.', defaultEnabled:false, defaultAction:'flag', phase:'output'},
  {name:'webhook', label:'Webhook (custom)', desc:'POST a request/response snippet to your own HTTPS endpoint (500ms timeout, fails open, HMAC-SHA256 signed). Checks both requests and responses.', defaultEnabled:false, defaultAction:'flag', phase:'input'},
];
// parseGuardrailRows builds the editable row state from the org's stored
// guardrails_config ("[]" = no custom override yet, show the platform
// defaults as an editable starting point; a real array = the org's exact
// custom config, unlisted rules shown unchecked). Any entry in the stored
// config whose rule name is NOT in GUARDRAIL_RULE_DEFS (e.g. a rule enabled
// via direct API call ahead of this UI knowing about it) is preserved as a
// hidden row (marked __unknownRule, carrying the original raw entry in
// __raw) so buildGuardrailsConfig can round-trip it verbatim on save
// instead of silently dropping it — see the C-2 destructive-save bug this
// fixes.
function parseGuardrailRows(configJSON){
  var arr=[]; try{ arr=JSON.parse(configJSON||'[]'); }catch(e){ arr=[]; }
  var byName={}; arr.forEach(function(r){ if(r&&r.name) byName[r.name]=r; });
  var knownNames={}; GUARDRAIL_RULE_DEFS.forEach(function(d){ knownNames[d.name]=true; });
  var hasCustom = arr.length>0;
  var rows = GUARDRAIL_RULE_DEFS.map(function(d){
    var r=byName[d.name];
    if(r) return {name:d.name, label:d.label, desc:d.desc, phase:d.phase, enabled:true, action:r.action||d.defaultAction,
      maxChars:String((r.params&&r.params.max_chars)||50000), patterns:((r.params&&r.params.patterns)||[]).join('\n'),
      required:((r.params&&r.params.required)||[]).join(','),
      webhookUrl:(r.params&&r.params.url)||'', webhookSecret:(r.params&&r.params.secret)||'',
      systemPrompt:(r.params&&r.params.system_prompt)||'',
      maxImageSizeBytes:(r.params&&r.params.max_image_size_bytes!=null)?String(r.params.max_image_size_bytes):'',
      allowedMimeTypes:((r.params&&r.params.allowed_mime_types)||[]).join(',')};
    return {name:d.name, label:d.label, desc:d.desc, phase:d.phase, enabled: hasCustom?false:d.defaultEnabled, action:d.defaultAction,
      maxChars:'50000', patterns:'', required:'', webhookUrl:'', webhookSecret:'', systemPrompt:'', maxImageSizeBytes:'', allowedMimeTypes:''};
  });
  arr.forEach(function(r){
    if(r && r.name && !knownNames[r.name]) rows.push({name:r.name, __unknownRule:true, __raw:r});
  });
  return rows;
}
// buildGuardrailsConfig turns the editable rows back into the RuleConfig[]
// payload the server expects. Rows marked __unknownRule are rules this UI
// doesn't recognize (see parseGuardrailRows) — they are ALWAYS included
// verbatim, regardless of any "enabled" state, so a save from this UI can
// never remove a rule it doesn't understand.
function buildGuardrailsConfig(rows){
  return rows.filter(function(r){return r.__unknownRule||r.enabled;}).map(function(r){
    if(r.__unknownRule) return r.__raw;
    var cfg={name:r.name, action:r.action};
    if(r.name==='length-cap'||r.name==='output-length-cap'){
      var mc=parseInt(r.maxChars,10);
      cfg.params={max_chars: mc>0?mc:50000};
    }
    if(r.name==='denylist'||r.name==='output-denylist'){
      cfg.params={patterns:(r.patterns||'').split('\n').map(function(p){return p.trim();}).filter(Boolean)};
    }
    if(r.name==='json-schema'){
      cfg.params={required:(r.required||'').split(',').map(function(k){return k.trim();}).filter(Boolean)};
    }
    if(r.name==='webhook'){
      cfg.params={url:(r.webhookUrl||'').trim(), secret:r.webhookSecret||''};
    }
    if(r.name==='moderation' && (r.systemPrompt||'').trim()){
      cfg.params={system_prompt:r.systemPrompt.trim()};
    }
    if(r.name==='vision'){
      var params={};
      if((r.maxImageSizeBytes||'').trim()!==''){
        var mb=parseInt(r.maxImageSizeBytes,10);
        if(!isNaN(mb)) params.max_image_size_bytes=mb;
      }
      var mimes=(r.allowedMimeTypes||'').split(',').map(function(m){return m.trim();}).filter(Boolean);
      if(mimes.length>0) params.allowed_mime_types=mimes;
      if(Object.keys(params).length>0) cfg.params=params;
    }
    return cfg;
  });
}

// AlertsPanel is org-scoped: pass orgId for the admin (any-org) surface, omit
// it for the customer (own-org) surface — same component, different NR_API
// calls under the hood, mirroring the guardrails pattern in admin.jsx/portal-team.jsx.
function AlertsPanel({orgId}){
  var t=useToast();
  var _rules=useState([]); var rules=_rules[0], setRules=_rules[1];
  var _events=useState([]); var events=_events[0], setEvents=_events[1];
  var _loading=useState(true); var loading=_loading[0], setLoading=_loading[1];
  var _showAdd=useState(false); var showAdd=_showAdd[0], setShowAdd=_showAdd[1];
  var _newRule=useState({rule_type:'budget_pct', threshold:80, channel_type:'webhook', channel_target:'', cooldown_minutes:60});
  var newRule=_newRule[0], setNewRule=_newRule[1];
  var _saving=useState(false); var saving=_saving[0], setSaving=_saving[1];

  var api = orgId ? {
    list: function(){return NR_API.adminAlertRules(orgId);},
    create: function(r){return NR_API.adminCreateAlertRule(orgId,r);},
    update: function(id,r){return NR_API.adminUpdateAlertRule(orgId,id,r);},
    del: function(id){return NR_API.adminDeleteAlertRule(orgId,id);},
    events: function(){return NR_API.adminAlertEvents(orgId);},
  } : {
    list: function(){return NR_API.alertRules();},
    create: function(r){return NR_API.createAlertRule(r);},
    update: function(id,r){return NR_API.updateAlertRule(id,r);},
    del: function(id){return NR_API.deleteAlertRule(id);},
    events: function(){return NR_API.alertEvents();},
  };

  var load=function(){
    setLoading(true);
    Promise.all([api.list(), api.events()]).then(function(res){
      setRules((res[0]&&res[0].rules)||[]);
      setEvents((res[1]&&res[1].events)||[]);
      setLoading(false);
    }).catch(function(){ setLoading(false); });
  };
  useEffect(load,[orgId]);

  var addRule=function(){
    setSaving(true);
    api.create(newRule).then(function(res){
      setSaving(false);
      if(res && res.id){ t({title:'Alert rule created'}); setShowAdd(false); setNewRule({rule_type:'budget_pct', threshold:80, channel_type:'webhook', channel_target:'', cooldown_minutes:60}); load(); }
      else t({kind:'error',title:'Failed to create alert rule'});
    }).catch(function(){ setSaving(false); t({kind:'error',title:'Failed to create alert rule'}); });
  };

  var toggleRule=function(r){
    var patch=Object.assign({},r,{enabled:!r.enabled});
    api.update(r.id, patch).then(function(ok){
      if(ok) setRules(function(rs){return rs.map(function(x){return x.id===r.id?patch:x;});});
      else t({kind:'error',title:'Failed to update rule'});
    });
  };

  var removeRule=function(id){
    api.del(id).then(function(ok){
      if(ok){ setRules(function(rs){return rs.filter(function(x){return x.id!==id;});}); t({kind:'info',title:'Alert rule deleted'}); }
      else t({kind:'error',title:'Failed to delete rule'});
    });
  };

  if(loading) return <div className="faint" style={{fontSize:13}}>Loading alerts…</div>;

  return (
    <div className="grid" style={{gap:14}}>
      {rules.length===0 && !showAdd && <div className="faint" style={{fontSize:13}}>No alert rules configured yet.</div>}
      {rules.map(function(r){
        var d=alertTypeDef(r.rule_type);
        return <div key={r.id} className="spread" style={{padding:'8px 0',borderTop:'1px solid rgba(128,128,128,.2)'}}>
          <div>
            <div style={{fontWeight:600}}>{d.label} {r.threshold}{d.unit}</div>
            <div className="faint" style={{fontSize:11.5}}>via {r.channel_type} → {r.channel_target} · cooldown {r.cooldown_minutes}m</div>
          </div>
          <div className="row" style={{gap:8}}>
            <Switch on={r.enabled} onChange={function(){toggleRule(r);}}/>
            <button className="iconbtn" title="Delete" onClick={function(){removeRule(r.id);}}><Icon name="trash" size={14}/></button>
          </div>
        </div>;
      })}

      {!showAdd ? <button className="btn btn-soft btn-sm" style={{alignSelf:'flex-start'}} onClick={function(){setShowAdd(true);}}><Icon name="onboard" size={14}/>Add alert rule</button> :
      <div className="codeblock" style={{padding:14}}>
        <div className="grid" style={{gridTemplateColumns:'1fr 1fr',gap:12}}>
          <Field label="Type"><select className="select" value={newRule.rule_type} onChange={function(e){setNewRule(Object.assign({},newRule,{rule_type:e.target.value}));}}>
            {ALERT_RULE_TYPES.map(function(d){return <option key={d.value} value={d.value}>{d.label}</option>;})}
          </select></Field>
          <Field label={'Threshold'+(alertTypeDef(newRule.rule_type).unit?(' ('+alertTypeDef(newRule.rule_type).unit+')'):'')}>
            <input className="input" type="number" value={newRule.threshold} onChange={function(e){setNewRule(Object.assign({},newRule,{threshold:parseFloat(e.target.value)||0}));}}/>
          </Field>
          <Field label="Channel"><select className="select" value={newRule.channel_type} onChange={function(e){setNewRule(Object.assign({},newRule,{channel_type:e.target.value}));}}>
            <option value="webhook">Webhook</option>
            <option value="email">Email</option>
          </select></Field>
          <Field label={newRule.channel_type==='email'?'Email address':'Webhook URL'}>
            <input className="input" placeholder={newRule.channel_type==='email'?'alerts@yourcompany.com':'https://hooks.slack.com/...'}
              value={newRule.channel_target} onChange={function(e){setNewRule(Object.assign({},newRule,{channel_target:e.target.value}));}}/>
          </Field>
        </div>
        <div className="faint" style={{fontSize:11,marginTop:2}}>{alertTypeDef(newRule.rule_type).hint}</div>
        <div className="row" style={{gap:8,marginTop:12,justifyContent:'flex-end'}}>
          <button className="btn btn-ghost btn-sm" onClick={function(){setShowAdd(false);}}>Cancel</button>
          <button className="btn btn-primary btn-sm" disabled={saving||!newRule.channel_target} onClick={addRule}>{saving?'Adding…':'Add rule'}</button>
        </div>
      </div>}

      {events.length>0 && <div style={{marginTop:8}}>
        <div className="faint" style={{fontSize:11,textTransform:'uppercase',letterSpacing:'.04em',marginBottom:8}}>Recent fired alerts</div>
        {events.slice(0,10).map(function(e){
          return <div key={e.id} style={{padding:'6px 0',borderTop:'1px solid rgba(128,128,128,.15)'}}>
            <div className="spread">
              <span style={{fontSize:12.5}}>{e.message}</span>
              <Badge color={e.delivered?'green':'red'} style={{fontSize:10}}>{e.delivered?'delivered':'failed'}</Badge>
            </div>
            <div className="faint" style={{fontSize:11}}>{new Date(e.fired_at).toLocaleString()}</div>
          </div>;
        })}
      </div>}
    </div>
  );
}

Object.assign(window,{ToastHost,useToast,Modal,Badge,TaskBadge,Switch,Field,CopyBtn,
  ProviderLogo,Metric,LineDual,BarStacked,BarSimple,Donut,Gauge,Spark,COL,
  PageHead,MiniBar,fmtUSD,fmtNum,fmtTok,
  LoadingPage,EmptyState,HealthSidebar,SectionCard,AlertsPanel});
