/* ============================================================
   NeuroRoute — Evals (EPIC F, Task 12): suite editor, run launcher,
   comparison view. Wired to the live /v1/customer/evals/* API
   (internal/gateway/eval_endpoints.go + eval_run_endpoints.go).

   Three sub-views inside one page component (local state, matching
   portal-team.jsx's convention rather than separate hash routes):
     'suites'   — suite list + recent runs (default)
     'launcher' — candidate builder for a chosen suite
     'run'      — polling run-status + comparison table

   Response shapes (from the Go handlers, not guessed):
     GET  /evals/suites        -> [{id,name,description,created_by,created_at,updated_at}]
     GET  /evals/suites/{id}   -> {suite:{...same...}, cases:[{id,seq,prompt,system_prompt,scorer,scorer_config}]}
     POST /evals/suites        -> 201 {suite_id} | 4xx {error}
     POST /evals/runs          -> 202 {run_id,status:"pending"} | 4xx {error}
     GET  /evals/runs          -> [{id,suite_id,status,candidates,aggregates,created_by,created_at,started_at,finished_at,error}]
     GET  /evals/runs/{id}     -> {run:{...same as list item...}, results:[{candidate_idx,case_id,status,score,cost_usd,latency_ms,input_tokens,output_tokens,selected_model,routing_decision,response,error}]}
     POST /evals/runs/{id}/cancel -> {status:"canceled"}
   `run.aggregates` is a JSON OBJECT keyed by candidate index as a STRING
   ("0","1",...) — Go's json.Marshal(map[int]Aggregate) — each value is
   {candidate_idx,passed,failed,unscored,errored,avg_score,total_cost_usd,p50_latency_ms,p95_latency_ms}.
   Run status is one of pending|running|complete|canceled (case status is a
   separate enum: passed|failed|unscored|errored).
   ============================================================ */

/* ── Local API helpers ──────────────────────────────────────
   This task is scoped to evals.jsx/index.html/app.jsx only, so these calls
   live here rather than joining window.NR_API in data.js — but they mirror
   data.js's `api()`/`post()` helpers exactly: credentials included, and a
   401 bounces to login via the same window.__nrSessionExpired hook the rest
   of the app uses (see data.js's `api()`). Mutating calls return
   {ok,status,data} (not collapsed to null on a non-2xx) so the caller can
   surface the API's own validation error message inline. */
function evalGet(path){
  // Returns {ok,status,data} — same shape as evalMutate below — and NEVER
  // rejects, so a caller can tell "genuinely empty" (ok:true, data:[]) apart
  // from "the fetch failed" (ok:false) instead of both collapsing to the
  // same falsy value.
  return fetch(path,{credentials:'include'}).then(function(r){
    if(r.status===401){ if(typeof window.__nrSessionExpired==='function') window.__nrSessionExpired(); return {ok:false, status:401, data:null}; }
    return r.json().catch(function(){return null;}).then(function(data){ return {ok:r.ok, status:r.status, data:data}; });
  }).catch(function(){ return {ok:false, status:0, data:null}; });
}
function evalMutate(path, method, body){
  var opts={method:method, credentials:'include'};
  if(body!==undefined){ opts.headers={'Content-Type':'application/json'}; opts.body=JSON.stringify(body); }
  return fetch(path, opts).then(function(r){
    if(r.status===401){ if(typeof window.__nrSessionExpired==='function') window.__nrSessionExpired(); return {ok:false, status:401, data:null}; }
    return r.json().catch(function(){return null;}).then(function(data){ return {ok:r.ok, status:r.status, data:data}; });
  }).catch(function(){ return {ok:false, status:0, data:null}; });
}
var EvalAPI = {
  suites:      function(){ return evalGet('/v1/customer/evals/suites'); },
  createSuite: function(payload){ return evalMutate('/v1/customer/evals/suites','POST',payload); },
  getSuite:    function(id){ return evalGet('/v1/customer/evals/suites/'+encodeURIComponent(id)); },
  deleteSuite: function(id){ return evalMutate('/v1/customer/evals/suites/'+encodeURIComponent(id),'DELETE').then(function(r){return r.ok;}); },
  runs:        function(){ return evalGet('/v1/customer/evals/runs'); },
  createRun:   function(payload){ return evalMutate('/v1/customer/evals/runs','POST',payload); },
  getRun:      function(id){ return evalGet('/v1/customer/evals/runs/'+encodeURIComponent(id)); },
  cancelRun:   function(id){ return evalMutate('/v1/customer/evals/runs/'+encodeURIComponent(id)+'/cancel','POST',{}).then(function(r){return r.ok;}); },
  exportUrl:   function(id,format){ return '/v1/customer/evals/runs/'+encodeURIComponent(id)+'/export?format='+(format||'csv'); },
  // Published routing-config versions (internal/gateway/routing_config_endpoints.go)
  // — used to populate the config-version candidate picker as a real
  // dropdown instead of free text. -> {versions:[{version,config,created_at,created_by}]}
  routingConfigVersions: function(){ return evalGet('/v1/customer/routing-config/versions'); },
};

/* EvalLoadError — shown when a list/detail fetch itself failed (network,
   5xx, etc.), as opposed to a fetch that succeeded and genuinely returned no
   rows. Deliberately NOT EmptyState: distinct icon/copy/color and a Retry
   action so a transient failure never looks like "there's nothing here". */
function EvalLoadError({what, onRetry}){
  return (
    <div className="card" style={{padding:'32px 24px',textAlign:'center'}}>
      <Icon name="alert" size={22} style={{color:'var(--red)'}}/>
      <p style={{margin:'10px 0 14px',fontSize:13.5}}>Failed to load {what}. This may be a temporary issue.</p>
      <button className="btn btn-soft btn-sm" onClick={onRetry}><Icon name="refresh" size={13}/>Retry</button>
    </div>
  );
}

var EVAL_SCORERS = ['exact', 'regex', 'json-schema', 'llm-judge'];
var EVAL_SCORER_TEMPLATES = {
  'exact': '{\n  "expected": "Paris",\n  "case_sensitive": false\n}',
  'regex': '{\n  "pattern": "^\\\\d{3}-\\\\d{4}$"\n}',
  'json-schema': '{\n  "required": ["name", "amount"]\n}',
  'llm-judge': '{\n  "rubric": "Does the response correctly and concisely answer the question?",\n  "threshold": 0.7\n}',
};
function newEvalCase(){
  return {prompt:'', system_prompt:'', scorer:'exact', scorer_config:EVAL_SCORER_TEMPLATES.exact};
}
function newEvalCandidate(){
  return {type:'model', model:'', strategy:'balanced', config_version:''};
}
function evalStrategyValue(label){ return label.toLowerCase().replace(/ /g,'-'); }
function evalCandidatePayload(c){
  if(c.type==='model') return {model:c.model};
  if(c.type==='strategy') return {strategy:c.strategy};
  return {config_version:c.config_version};
}
function evalCandidateValid(c){
  if(c.type==='model') return !!(c.model && c.model.trim());
  if(c.type==='strategy') return !!(c.strategy && c.strategy.trim());
  return !!(c.config_version && c.config_version.trim());
}
function evalCandidateLabel(c){
  if(!c) return 'unknown';
  if(c.model) return (window.DATA.modelById(c.model).name || c.model);
  if(c.strategy) return 'Strategy: '+c.strategy;
  if(c.config_version) return 'Config: '+c.config_version;
  return 'unknown';
}
function evalRunStatusColor(status){
  // Run-level status is only ever pending|running|complete|canceled — case-
  // level status (passed|failed|unscored|errored) is handled separately by
  // evalCaseStatusColor below.
  return ({pending:'gray', running:'blue', complete:'green', canceled:'gray'})[status] || 'gray';
}
function evalCaseStatusColor(status){
  return ({passed:'green', failed:'red', unscored:'amber', errored:'red'})[status] || 'gray';
}

/* =====================  SUITE EDITOR MODAL  ===================== */
function EvalSuiteModal({onClose, onCreated}){
  var t=useToast();
  var _name=useState(''); var name=_name[0], setName=_name[1];
  var _desc=useState(''); var desc=_desc[0], setDesc=_desc[1];
  var _cases=useState([newEvalCase()]); var cases=_cases[0], setCases=_cases[1];
  var _saving=useState(false); var saving=_saving[0], setSaving=_saving[1];
  var _error=useState(''); var error=_error[0], setError=_error[1];

  var patchCase=function(i,patch){
    setCases(cases.map(function(c,idx){return idx===i?Object.assign({},c,patch):c;}));
  };
  var setCaseScorer=function(i,scorer){
    patchCase(i,{scorer:scorer, scorer_config:EVAL_SCORER_TEMPLATES[scorer]||'{}'});
  };
  var addCase=function(){ setCases(cases.concat([newEvalCase()])); };
  var removeCase=function(i){ setCases(cases.filter(function(_,idx){return idx!==i;})); };

  var submit=function(){
    if(!name.trim()){ setError('Name is required.'); return; }
    if(cases.length===0){ setError('At least one case is required.'); return; }
    var payloadCases=[];
    for(var i=0;i<cases.length;i++){
      var c=cases[i];
      if(!c.prompt.trim()){ setError('Case '+(i+1)+': prompt is required.'); return; }
      var cfg;
      try{ cfg = c.scorer_config.trim()? JSON.parse(c.scorer_config) : {}; }
      catch(e){ setError('Case '+(i+1)+': scorer config is not valid JSON — '+e.message); return; }
      payloadCases.push({prompt:c.prompt, system_prompt:c.system_prompt||'', scorer:c.scorer, scorer_config:cfg});
    }
    setSaving(true); setError('');
    EvalAPI.createSuite({name:name.trim(), description:desc.trim(), cases:payloadCases}).then(function(res){
      setSaving(false);
      if(res.ok && res.data && res.data.suite_id){
        t({title:'Suite created', msg:name.trim()});
        onCreated();
      } else {
        var msg = (res.data && res.data.error) || (res.status===403?'You need an org admin role to create suites.':'Failed to create suite.');
        setError(msg);
      }
    }).catch(function(){ setSaving(false); setError('Failed to create suite — network error.'); });
  };

  return (
    <Modal title="New eval suite" sub="A suite is a fixed set of prompts + scorers you re-run against any candidate model, strategy, or routing config."
      onClose={onClose} lg
      footer={<><button className="btn btn-ghost" onClick={onClose}>Cancel</button>
        <button className="btn btn-primary" disabled={saving} onClick={submit}>{saving?'Creating…':'Create suite'}</button></>}>
      <div className="grid" style={{gap:16}}>
        {error && <div style={{color:'var(--red)',fontSize:13}}>{error}</div>}
        <div className="grid" style={{gridTemplateColumns:'1fr 2fr',gap:14}}>
          <Field label="Name"><input className="input" placeholder="Support-reply quality" value={name} onChange={function(e){setName(e.target.value);}}/></Field>
          <Field label="Description (optional)"><input className="input" placeholder="Checks tone + accuracy on canned support tickets" value={desc} onChange={function(e){setDesc(e.target.value);}}/></Field>
        </div>
        <div>
          <div className="spread" style={{marginBottom:8}}>
            <div style={{fontWeight:600,fontSize:13.5}}>Cases ({cases.length})</div>
            <button className="btn btn-soft btn-sm" onClick={addCase}><Icon name="plus" size={13}/>Add case</button>
          </div>
          <div className="grid" style={{gap:12}}>
            {cases.map(function(c,i){
              return (
                <div key={i} className="card" style={{padding:14}}>
                  <div className="spread" style={{marginBottom:8}}>
                    <b style={{fontSize:12.5}}>Case {i+1}</b>
                    {cases.length>1 && <button className="iconbtn" onClick={function(){removeCase(i);}} title="Remove case"><Icon name="trash" size={13}/></button>}
                  </div>
                  <div className="grid" style={{gap:10}}>
                    <Field label="Prompt"><textarea className="input" rows={2} style={{resize:'vertical'}}
                      placeholder="What is the capital of France?" value={c.prompt}
                      onChange={function(e){patchCase(i,{prompt:e.target.value});}}/></Field>
                    <Field label="System prompt (optional)"><textarea className="input" rows={1} style={{resize:'vertical'}}
                      placeholder="Answer in one word." value={c.system_prompt}
                      onChange={function(e){patchCase(i,{system_prompt:e.target.value});}}/></Field>
                    <div className="grid" style={{gridTemplateColumns:'1fr 2fr',gap:10}}>
                      <Field label="Scorer">
                        <select className="select" value={c.scorer} onChange={function(e){setCaseScorer(i,e.target.value);}}>
                          {EVAL_SCORERS.map(function(s){return <option key={s} value={s}>{s}</option>;})}
                        </select>
                      </Field>
                      <Field label="Scorer config (JSON)" hint="Changing the scorer resets this to an example — edit it in place.">
                        <textarea className="input mono" rows={3} style={{fontSize:12,resize:'vertical'}} spellCheck={false}
                          value={c.scorer_config} onChange={function(e){patchCase(i,{scorer_config:e.target.value});}}/>
                      </Field>
                    </div>
                  </div>
                </div>
              );
            })}
          </div>
        </div>
      </div>
    </Modal>
  );
}

/* =====================  RUN LAUNCHER  ===================== */
function EvalRunLauncher({suite, onBack, onLaunched}){
  var t=useToast();
  var _cands=useState([newEvalCandidate()]); var cands=_cands[0], setCands=_cands[1];
  var _launching=useState(false); var launching=_launching[0], setLaunching=_launching[1];
  var _error=useState(''); var error=_error[0], setError=_error[1];
  // Published routing-config versions, fetched on mount the same way
  // window.DATA.MODELS is already available — populates the config-version
  // candidate picker as a real dropdown instead of free text.
  var _cfgVersions=useState({loading:true, list:[], failed:false}); var cfgVersions=_cfgVersions[0], setCfgVersions=_cfgVersions[1];

  var loadCfgVersions=function(){
    setCfgVersions({loading:true, list:[], failed:false});
    EvalAPI.routingConfigVersions().then(function(res){
      if(res.ok && res.data && Array.isArray(res.data.versions)) setCfgVersions({loading:false, list:res.data.versions, failed:false});
      else setCfgVersions({loading:false, list:[], failed:true});
    });
  };
  useEffect(function(){ loadCfgVersions(); },[]);

  var patch=function(i,p){ setCands(cands.map(function(c,idx){return idx===i?Object.assign({},c,p):c;})); };
  var addCand=function(){ if(cands.length<6) setCands(cands.concat([newEvalCandidate()])); };
  var removeCand=function(i){ setCands(cands.filter(function(_,idx){return idx!==i;})); };

  var submit=function(){
    if(cands.length===0){ setError('Add at least one candidate.'); return; }
    for(var i=0;i<cands.length;i++){
      if(!evalCandidateValid(cands[i])){ setError('Candidate '+(i+1)+' is missing a value.'); return; }
    }
    setLaunching(true); setError('');
    EvalAPI.createRun({suite_id:suite.id, candidates:cands.map(evalCandidatePayload)}).then(function(res){
      setLaunching(false);
      if(res.ok && res.data && res.data.run_id){
        t({title:'Run started', msg:suite.name});
        onLaunched(res.data.run_id);
      } else {
        var msg=(res.data && res.data.error) || (res.status===403?'You need an org admin role to run evals.':'Failed to start run.');
        setError(msg);
      }
    }).catch(function(){ setLaunching(false); setError('Failed to start run — network error.'); });
  };

  return (
    <div className="grid" style={{gap:18}}>
      <PageHead desc={'Launching a run for "'+suite.name+'" — add up to 6 candidates to compare side by side.'}>
        <button className="btn btn-ghost btn-sm" onClick={onBack}>Back to suites</button>
      </PageHead>
      <SectionCard title="Candidates" sub={cands.length+' of 6'}
        action={cands.length<6 && <button className="btn btn-soft btn-sm" onClick={addCand}><Icon name="plus" size={13}/>Add candidate</button>}>
        <div className="grid" style={{gap:12}}>
          {error && <div style={{color:'var(--red)',fontSize:13}}>{error}</div>}
          {cands.map(function(c,i){
            return (
              <div key={i} className="card" style={{padding:14}}>
                <div className="spread" style={{marginBottom:10}}>
                  <b style={{fontSize:12.5}}>Candidate {i+1}</b>
                  {cands.length>1 && <button className="iconbtn" onClick={function(){removeCand(i);}} title="Remove candidate"><Icon name="trash" size={13}/></button>}
                </div>
                <div className="row" style={{gap:16,marginBottom:10,flexWrap:'wrap'}}>
                  {['model','strategy','config_version'].map(function(ty){
                    return <label key={ty} className="row" style={{gap:6,fontSize:13,cursor:'pointer'}}>
                      <input type="radio" name={'cand-type-'+i} checked={c.type===ty} onChange={function(){patch(i,{type:ty});}}/>
                      {ty==='model'?'Model':ty==='strategy'?'Strategy':'Config version'}
                    </label>;
                  })}
                </div>
                {c.type==='model' && <Field label="Model">
                  <select className="select" value={c.model} onChange={function(e){patch(i,{model:e.target.value});}}>
                    <option value="">— choose a model —</option>
                    {window.DATA.MODELS.map(function(m){return <option key={m.id} value={m.id}>{m.name} ({window.DATA.providerById(m.provider).name})</option>;})}
                  </select>
                </Field>}
                {c.type==='strategy' && <Field label="Strategy">
                  <select className="select" value={c.strategy} onChange={function(e){patch(i,{strategy:e.target.value});}}>
                    {window.DATA.STRATEGIES.map(function(s){return <option key={s} value={evalStrategyValue(s)}>{s}</option>;})}
                  </select>
                </Field>}
                {c.type==='config_version' && <Field label="Config version" hint={
                    cfgVersions.loading ? 'Loading published versions…'
                    : cfgVersions.failed ? "Couldn't load published versions."
                    : cfgVersions.list.length===0 ? 'No published routing configs yet — publish one from Settings → Routing config.'
                    : 'Matched against your published routing-config versions (Settings → Routing config).'
                  }>
                  {cfgVersions.failed ?
                    <button type="button" className="btn btn-soft btn-sm" onClick={loadCfgVersions}><Icon name="refresh" size={13}/>Retry</button> :
                    <select className="select" disabled={cfgVersions.loading || cfgVersions.list.length===0}
                      value={c.config_version} onChange={function(e){patch(i,{config_version:e.target.value});}}>
                      <option value="">— choose a version —</option>
                      {cfgVersions.list.map(function(v){
                        return <option key={v.version} value={String(v.version)}>v{v.version}{v.created_by?(' · '+v.created_by):''}</option>;
                      })}
                    </select>}
                </Field>}
              </div>
            );
          })}
        </div>
        <div className="row" style={{justifyContent:'flex-end',marginTop:16}}>
          <button className="btn btn-primary" disabled={launching} onClick={submit}>{launching?'Starting…':'Run'}</button>
        </div>
      </SectionCard>
    </div>
  );
}

/* =====================  RUN VIEW  ===================== */
function EvalRunView({runId, onBack}){
  var t=useToast();
  var _loading=useState(true); var loading=_loading[0], setLoading=_loading[1];
  var _data=useState(null); var data=_data[0], setData=_data[1]; // {run, results}
  var _caseMap=useState({}); var caseMap=_caseMap[0], setCaseMap=_caseMap[1]; // case_id -> {seq, prompt}
  var _drillIdx=useState(null); var drillIdx=_drillIdx[0], setDrillIdx=_drillIdx[1]; // candidate idx whose cases are shown in a modal
  // initError distinguishes a genuine "run not found / no access" (404) from
  // any other load failure (network error, 5xx, etc.) so a transient failure
  // doesn't get mislabeled as "this run doesn't exist" — the exact conflation
  // EvalLoadError exists elsewhere in this file to avoid. Only meaningful
  // before the run has ever successfully loaded (see the `!data` guard in
  // `load` below) — once we have real data, a later poll hiccup must not
  // blow away a working view.
  var _initError=useState(null); var initError=_initError[0], setInitError=_initError[1];
  var pollRef=useRef(null);
  // `load` is invoked from three places (the mount effect below, the poll
  // interval, and cancelRun's follow-up refresh) rather than living inside a
  // single effect, so a plain per-effect `cancelled` closure var (like the
  // one the suite-fetch effect below uses) can't guard it — a component-
  // lifetime ref flipped false on unmount is the same "cancelled flag"
  // pattern generalized to a function with multiple call sites.
  var aliveRef=useRef(true);
  useEffect(function(){ return function(){ aliveRef.current=false; }; },[]);

  var load=function(){
    return EvalAPI.getRun(runId).then(function(res){
      if(!aliveRef.current) return res;
      if(res.ok && res.data && res.data.run){
        setData(res.data);
        setInitError(null);
      } else if(!data){
        // Only record this as an "initial load failed" state while we've
        // never had real data — a poll-cycle hiccup after a successful load
        // must not overwrite a working run view.
        setInitError({status:res.status});
      }
      setLoading(false);
      return res;
    });
  };

  useEffect(function(){
    var cancelled=false;
    load();
    // Best-effort: pull the suite's cases so results can show the prompt
    // instead of a bare case UUID. Failure here is silent — the run view
    // still works with just case_id.
    EvalAPI.getRun(runId).then(function(res){
      if(!res.ok || !res.data || !res.data.run || !res.data.run.suite_id) return;
      EvalAPI.getSuite(res.data.run.suite_id).then(function(sres){
        if(cancelled || !sres.ok || !sres.data || !sres.data.cases) return;
        var m={};
        sres.data.cases.forEach(function(c){ m[c.id]={seq:c.seq, prompt:c.prompt}; });
        setCaseMap(m);
      });
    });
    return function(){ cancelled=true; };
  },[runId]);

  useEffect(function(){
    var status = data && data.run && data.run.status;
    if(status==='pending' || status==='running'){
      pollRef.current = setInterval(function(){ load(); }, 2000);
      return function(){ clearInterval(pollRef.current); };
    }
  },[data && data.run && data.run.status]);

  var cancelRun=function(){
    EvalAPI.cancelRun(runId).then(function(res){
      if(res){ t({kind:'info',title:'Run canceled'}); load(); }
      else t({kind:'error',title:'Failed to cancel run'});
    }).catch(function(){ t({kind:'error',title:'Failed to cancel run'}); });
  };

  if(loading) return <LoadingPage/>;
  if(!data || !data.run){
    // A 404 (or the wire convention this codebase uses elsewhere to hide
    // cross-tenant existence) means genuinely not found / no access; any
    // other failure (network error, 5xx, ...) is a load error worth retrying
    // rather than telling the user the run doesn't exist.
    if(initError && initError.status!==404){
      return <EvalLoadError what="this run" onRetry={load}/>;
    }
    return <EmptyState icon="alert" title="Run not found" desc="This run doesn't exist or you don't have access to it."
      actions={[{label:'Back to suites', onClick:onBack, primary:true}]}/>;
  }

  var run=data.run, results=data.results||[];
  var status=run.status;
  var candidates=run.candidates||[];
  var aggByIdx={};
  if(run.aggregates){
    Object.keys(run.aggregates).forEach(function(k){ aggByIdx[k]=run.aggregates[k]; });
  }
  var resultsByIdx={};
  results.forEach(function(r){
    (resultsByIdx[r.candidate_idx] = resultsByIdx[r.candidate_idx]||[]).push(r);
  });
  // Winner: highest pass rate, tie-broken by lowest total cost — only among
  // candidates that actually have at least one scored case so far. Stays -1
  // (no highlight) while a run is still pending/running with no results yet.
  var winnerIdx=-1, winnerRate=-1, winnerCost=Infinity;
  candidates.forEach(function(c,idx){
    var agg=aggByIdx[String(idx)];
    if(!agg) return;
    var total=agg.passed+agg.failed+agg.unscored+agg.errored;
    if(total<=0) return;
    var rate=agg.passed/total;
    if(rate>winnerRate || (rate===winnerRate && agg.total_cost_usd<winnerCost)){
      winnerRate=rate; winnerCost=agg.total_cost_usd; winnerIdx=idx;
    }
  });

  return (
    <div className="grid" style={{gap:18}}>
      <PageHead desc={'Run '+run.id.slice(0,8)+'… · started '+(run.created_at?timeAgo(run.created_at):'—')}>
        <button className="btn btn-ghost btn-sm" onClick={onBack}>Back to suites</button>
        {(status==='pending'||status==='running') && <button className="btn btn-soft btn-sm" onClick={cancelRun}><Icon name="x" size={13}/>Cancel</button>}
        <button className="btn btn-soft btn-sm" onClick={function(){window.open(EvalAPI.exportUrl(runId,'csv'),'_blank');}}><Icon name="download" size={13}/>CSV</button>
        <button className="btn btn-soft btn-sm" onClick={function(){window.open(EvalAPI.exportUrl(runId,'json'),'_blank');}}><Icon name="download" size={13}/>JSON</button>
      </PageHead>

      <SectionCard title="Status"
        action={<Badge color={evalRunStatusColor(status)} dot>{status}</Badge>}>
        {(status==='pending'||status==='running') && <div className="faint" style={{fontSize:13}}>Polling every 2s… {results.length} case result{results.length===1?'':'s'} recorded so far.</div>}
        {run.error && <div style={{color:'var(--red)',fontSize:13,marginTop:6}}>{run.error}</div>}
      </SectionCard>

      <SectionCard title="Comparison" sub={candidates.length+' candidate'+(candidates.length===1?'':'s')} pad={false}>
        <div className="scroll-x"><table className="table">
          <thead><tr>
            <th>Candidate</th><th>Pass rate</th><th>Avg score</th><th>Total cost</th>
            <th>p50 latency</th><th>Unscored</th><th>Errored</th><th></th>
          </tr></thead>
          <tbody>
            {candidates.map(function(c,idx){
              var agg=aggByIdx[String(idx)];
              var total = agg ? (agg.passed+agg.failed+agg.unscored+agg.errored) : 0;
              var passRate = agg && total>0 ? Math.round((agg.passed/total)*100)+'%' : '—';
              var isWinner = winnerIdx>=0 && idx===winnerIdx;
              return (
                <tr key={idx} style={isWinner?{background:'rgba(16,185,129,.08)'}:null}>
                  <td style={{color:'var(--tx-0)',fontWeight:600}}>
                    <span className="row" style={{gap:8}}>{evalCandidateLabel(c)}{isWinner && <Badge color="green" dot>Winner</Badge>}</span>
                  </td>
                  <td className="tnum" style={isWinner?{fontWeight:700,color:'var(--green)'}:null}>{passRate}</td>
                  <td className="tnum">{agg?agg.avg_score.toFixed(2):'—'}</td>
                  <td className="tnum">{agg?fmtUSD(agg.total_cost_usd,4):'—'}</td>
                  <td className="tnum">{agg?agg.p50_latency_ms+' ms':'—'}</td>
                  <td className="tnum">{agg?agg.unscored:'—'}</td>
                  <td className="tnum">{agg?agg.errored:'—'}</td>
                  <td style={{textAlign:'right'}}>
                    <button className="btn btn-ghost btn-sm" onClick={function(){setDrillIdx(idx);}}>
                      <Icon name="chevron" size={12}/>Cases
                    </button>
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table></div>
      </SectionCard>

      {drillIdx!=null && <Modal title={'Case results — '+evalCandidateLabel(candidates[drillIdx])}
        sub={(resultsByIdx[drillIdx]||[]).length+' case result'+((resultsByIdx[drillIdx]||[]).length===1?'':'s')}
        onClose={function(){setDrillIdx(null);}} lg
        footer={<button className="btn btn-ghost" onClick={function(){setDrillIdx(null);}}>Close</button>}>
        {(resultsByIdx[drillIdx]||[]).length===0 ? <p className="faint" style={{fontSize:13,margin:0}}>No case results yet.</p> :
          <div className="scroll-x"><table className="table" style={{fontSize:12.5}}>
            <thead><tr><th>Case</th><th>Status</th><th>Score</th><th>Cost</th><th>Latency</th><th>Model</th><th>Response</th></tr></thead>
            <tbody>
              {(resultsByIdx[drillIdx]||[]).map(function(r){
                var cinfo=caseMap[r.case_id];
                return <tr key={r.case_id}>
                  <td className="faint" style={{maxWidth:220}}>{cinfo?cinfo.prompt:r.case_id}</td>
                  <td><Badge color={evalCaseStatusColor(r.status)}>{r.status}</Badge></td>
                  <td className="tnum">{r.score!=null?r.score.toFixed(2):'—'}</td>
                  <td className="tnum">{fmtUSD(r.cost_usd,4)}</td>
                  <td className="tnum">{r.latency_ms} ms</td>
                  <td className="faint">{r.selected_model||'—'}</td>
                  <td className="faint" style={{maxWidth:280,whiteSpace:'pre-wrap'}}>{r.response||(r.error?<span style={{color:'var(--red)'}}>{r.error}</span>:'—')}</td>
                </tr>;
              })}
            </tbody>
          </table></div>}
      </Modal>}
    </div>
  );
}

/* =====================  SUITES LIST + PAGE ROOT  ===================== */
function EvalsPage(){
  var t=useToast();
  var _view=useState('suites'); var view=_view[0], setView=_view[1]; // 'suites'|'launcher'|'run'
  var _loading=useState(true); var loading=_loading[0], setLoading=_loading[1];
  var _suites=useState([]); var suites=_suites[0], setSuites=_suites[1];
  var _runs=useState([]); var runs=_runs[0], setRuns=_runs[1];
  var _modal=useState(false); var modal=_modal[0], setModal=_modal[1];
  var _confirmDelete=useState(null); var confirmDelete=_confirmDelete[0], setConfirmDelete=_confirmDelete[1];
  var _viewCases=useState(null); var viewCases=_viewCases[0], setViewCases=_viewCases[1]; // {suite, loading, cases, failed}
  var _launchSuite=useState(null); var launchSuite=_launchSuite[0], setLaunchSuite=_launchSuite[1];
  var _runId=useState(null); var runId=_runId[0], setRunId=_runId[1];
  var _suitesFailed=useState(false); var suitesFailed=_suitesFailed[0], setSuitesFailed=_suitesFailed[1];
  var _runsFailed=useState(false); var runsFailed=_runsFailed[0], setRunsFailed=_runsFailed[1];

  var loadAll=function(){
    return Promise.all([EvalAPI.suites(), EvalAPI.runs()]).then(function(res){
      var suitesRes=res[0], runsRes=res[1];
      if(suitesRes.ok && Array.isArray(suitesRes.data)){
        setSuites(suitesRes.data.slice().sort(function(a,b){return (b.created_at||'').localeCompare(a.created_at||'');}));
        setSuitesFailed(false);
      } else {
        setSuites([]);
        setSuitesFailed(true);
      }
      if(runsRes.ok && Array.isArray(runsRes.data)){
        setRuns(runsRes.data.slice().sort(function(a,b){return (b.created_at||'').localeCompare(a.created_at||'');}).slice(0,10));
        setRunsFailed(false);
      } else {
        setRuns([]);
        setRunsFailed(true);
      }
      setLoading(false);
    });
  };

  useEffect(function(){ loadAll(); },[]);

  var openCases=function(suite){
    setViewCases({suite:suite, loading:true, cases:[], failed:false});
    EvalAPI.getSuite(suite.id).then(function(res){
      if(res.ok && res.data && Array.isArray(res.data.cases)) setViewCases({suite:suite, loading:false, cases:res.data.cases, failed:false});
      else setViewCases({suite:suite, loading:false, cases:[], failed:true});
    });
  };

  var deleteSuite=function(){
    var s=confirmDelete;
    setConfirmDelete(null);
    EvalAPI.deleteSuite(s.id).then(function(ok){
      if(ok){ setSuites(function(list){return list.filter(function(x){return x.id!==s.id;});}); t({kind:'info',title:'Suite deleted',msg:s.name}); }
      else t({kind:'error',title:'Failed to delete suite',msg:'You may need an org admin role.'});
    }).catch(function(){ t({kind:'error',title:'Failed to delete suite'}); });
  };

  var suiteName=function(id){ var s=suites.filter(function(x){return x.id===id;})[0]; return s?s.name:id.slice(0,8)+'…'; };

  if(view==='launcher' && launchSuite){
    return <EvalRunLauncher suite={launchSuite}
      onBack={function(){setView('suites'); setLaunchSuite(null);}}
      onLaunched={function(id){ setRunId(id); setView('run'); loadAll(); }}/>;
  }
  if(view==='run' && runId){
    return <EvalRunView runId={runId} onBack={function(){setView('suites'); setRunId(null); loadAll();}}/>;
  }

  if(loading) return <LoadingPage/>;

  return (
    <div className="grid" style={{gap:18}}>
      <PageHead desc="Define a fixed set of prompts + scorers once, then run it against any model, strategy, or routing config to compare quality, cost, and latency side by side.">
        <button className="btn btn-primary btn-sm" onClick={function(){setModal(true);}}><Icon name="onboard" size={15}/>New suite</button>
      </PageHead>

      {suitesFailed ?
        <EvalLoadError what="your eval suites" onRetry={loadAll}/> :
       suites.length===0 ?
        <EmptyState icon="checkcircle" title="No eval suites yet"
          desc="Create a suite of prompts and scoring rules, then run it against candidate models or strategies to see which wins on quality, cost, and latency."
          actions={[{label:'New suite', onClick:function(){setModal(true);}, primary:true}]}/> :
        <SectionCard title="Suites" sub={suites.length+' total'} pad={false}>
          <div className="scroll-x"><table className="table">
            <thead><tr><th>Name</th><th>Description</th><th>Cases</th><th>Created</th><th style={{textAlign:'right'}}>Actions</th></tr></thead>
            <tbody>
              {suites.map(function(s){
                return (
                  <tr key={s.id}>
                    <td style={{color:'var(--tx-0)',fontWeight:600}}>{s.name}</td>
                    <td className="faint" style={{maxWidth:280}}>{s.description||'—'}</td>
                    <td>
                      <button className="btn btn-ghost btn-sm" onClick={function(){openCases(s);}}>
                        <Icon name="chevron" size={12}/>View cases
                      </button>
                    </td>
                    <td className="faint" style={{fontSize:12,whiteSpace:'nowrap'}}>{s.created_at?new Date(s.created_at).toLocaleDateString():'—'}</td>
                    <td style={{textAlign:'right'}}>
                      <div className="row" style={{gap:6,justifyContent:'flex-end'}}>
                        <button className="btn btn-soft btn-sm" onClick={function(){setLaunchSuite(s); setView('launcher');}}><Icon name="playground" size={13}/>Run</button>
                        <button className="iconbtn" onClick={function(){setConfirmDelete(s);}} title="Delete suite"><Icon name="trash" size={14}/></button>
                      </div>
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table></div>
        </SectionCard>}

      <SectionCard title="Recent runs" sub={runsFailed?'Failed to load':runs.length+' shown'} pad={false}>
        {runsFailed ? <EvalLoadError what="recent runs" onRetry={loadAll}/> :
         runs.length===0 ? <div className="empty" style={{padding:32}}><div className="e-ico"><Icon name="clock" size={22}/></div><p className="faint">No runs yet — launch one from a suite above.</p></div> :
          <table className="table">
            <thead><tr><th>Suite</th><th>Status</th><th>Candidates</th><th>Started</th><th style={{textAlign:'right'}}></th></tr></thead>
            <tbody>
              {runs.map(function(r){
                return <tr key={r.id}>
                  <td style={{color:'var(--tx-0)'}}>{suiteName(r.suite_id)}</td>
                  <td><Badge color={evalRunStatusColor(r.status)} dot>{r.status}</Badge></td>
                  <td className="faint">{(r.candidates||[]).length}</td>
                  <td className="faint" style={{fontSize:12}}>{r.created_at?timeAgo(r.created_at):'—'}</td>
                  <td style={{textAlign:'right'}}>
                    <button className="btn btn-soft btn-sm" onClick={function(){setRunId(r.id); setView('run');}}>View</button>
                  </td>
                </tr>;
              })}
            </tbody>
          </table>}
      </SectionCard>

      {modal && <EvalSuiteModal onClose={function(){setModal(false);}} onCreated={function(){setModal(false); loadAll();}}/>}

      {confirmDelete && <Modal title="Delete eval suite?" sub={'This permanently removes "'+confirmDelete.name+'" and all its cases. Past run results are unaffected.'}
        onClose={function(){setConfirmDelete(null);}}
        footer={<><button className="btn btn-ghost" onClick={function(){setConfirmDelete(null);}}>Cancel</button>
          <button className="btn btn-danger" onClick={deleteSuite}><Icon name="trash" size={14}/>Delete suite</button></>}>
        <p className="faint" style={{fontSize:13,margin:0}}>This cannot be undone.</p>
      </Modal>}

      {viewCases && <Modal title={'Cases — '+viewCases.suite.name}
        sub={viewCases.loading?'Loading…':viewCases.failed?'Failed to load':(viewCases.cases.length+' case'+(viewCases.cases.length===1?'':'s'))}
        onClose={function(){setViewCases(null);}} lg
        footer={<button className="btn btn-ghost" onClick={function(){setViewCases(null);}}>Close</button>}>
        {viewCases.loading ? <LoadingPage/> :
         viewCases.failed ? <EvalLoadError what="cases" onRetry={function(){openCases(viewCases.suite);}}/> :
         viewCases.cases.length===0 ? <p className="faint" style={{fontSize:13,margin:0}}>No cases.</p> :
          <div className="scroll-x"><table className="table" style={{fontSize:12.5}}>
            <thead><tr><th>#</th><th>Prompt</th><th>System prompt</th><th>Scorer</th></tr></thead>
            <tbody>
              {viewCases.cases.map(function(c){return <tr key={c.id}>
                <td className="faint">{c.seq}</td>
                <td style={{maxWidth:360,whiteSpace:'pre-wrap'}}>{c.prompt}</td>
                <td className="faint" style={{maxWidth:220,whiteSpace:'pre-wrap'}}>{c.system_prompt||'—'}</td>
                <td><Badge color="blue">{c.scorer}</Badge></td>
              </tr>;})}
            </tbody>
          </table></div>}
      </Modal>}
    </div>
  );
}

window.PAGES = Object.assign(window.PAGES||{}, {evals:EvalsPage});
