/* ============================================================
   NeuroRoute — Admin: Model Catalog Lifecycle

   Every model a provider sync has ever seen moves through:
     discovered -> ready -> active -> unavailable -> decommission_pending -> decommissioned
   with two sticky side-states: blocked (never activated, provider rejected it)
   and disabled (admin pulled it, regardless of provider status).

   Reads GET /v1/admin/model-catalog/discovered (activation_state + discovery
   metadata for every row) and drives the four state-mutating endpoints:
   activate / disable / probe / decommission. This is distinct from the older
   per-task quality-override editor on the Model Performance page — that CRUD
   never reads or writes activation_state, and every model regardless of
   lifecycle stage can have quality overrides set there.
   ============================================================ */

var LIFECYCLE_STATES = [
  {id:'discovered',          label:'Discovered',           color:'gray',
    desc:'Seen by a weekly provider sync. Not yet priced/capability-complete, so it can’t be activated.'},
  {id:'ready',                label:'Ready',                color:'cyan',
    desc:'Price + capabilities are complete. Awaiting an admin to activate it.'},
  {id:'active',               label:'Active',               color:'green',
    desc:'Live and routable.'},
  {id:'blocked',              label:'Blocked',              color:'red',
    desc:'A probe got 403/404 before this model was ever activated. Re-probed daily; flips back to Ready on its own if the provider enables it.'},
  {id:'unavailable',          label:'Unavailable',          color:'amber',
    desc:'Was active; a probe or a real request confirmed the provider no longer serves it. Routing already stopped. Re-probed hourly — returns to Active automatically if it comes back.'},
  {id:'decommission_pending', label:'Decommission Pending', color:'magenta',
    desc:'Unavailable for 48h+. Flagged for an admin to retire — no routing change, this is a flag only.'},
  {id:'decommissioned',       label:'Decommissioned',       color:'gray',
    desc:'Retired by an admin. Row is kept forever (historical billing references it) but can never be reactivated.'},
  {id:'disabled',             label:'Disabled',             color:'purple',
    desc:'Manually pulled from routing by an admin, regardless of provider status. Sticky — sync and probes never touch it. Recommission returns it to Ready; Decommission retires it for good.'},
];
var LIFECYCLE_STATE_BY_ID = {};
LIFECYCLE_STATES.forEach(function(s){ LIFECYCLE_STATE_BY_ID[s.id] = s; });

// lifecycleActions mirrors the exact server-side gating in
// internal/gateway/model_activation_endpoints.go / model_probe_endpoints.go /
// model_decommission_endpoints.go / model_recommission_endpoints.go, so a
// button is only ever shown when the call behind it can succeed:
//   - activate:     any state except active/disabled/decommissioned (422 if
//                    price/capabilities/quality/provider aren't all ready —
//                    the server explains which, this UI doesn't duplicate that check)
//   - disable:      always allowed except from the two sticky states
//                    (already disabled, or decommissioned)
//   - probe:        only blocked/ready/unavailable (404 otherwise — "not
//                    awaiting an availability decision")
//   - decommission: only unavailable/decommission_pending/disabled (409 otherwise)
//   - recommission: only disabled (409 otherwise). Deliberately NOT offered
//                    for decommissioned — that state is permanent by design
//                    (see RecommissionModel's doc comment in internal/db);
//                    disabled is the one sticky state with a way back.
function lifecycleActions(state){
  return {
    activate:     ['discovered','ready','blocked','unavailable','decommission_pending'].indexOf(state) >= 0,
    disable:      ['discovered','ready','active','blocked','unavailable','decommission_pending'].indexOf(state) >= 0,
    probe:        ['blocked','ready','unavailable'].indexOf(state) >= 0,
    decommission: ['unavailable','decommission_pending','disabled'].indexOf(state) >= 0,
    recommission: state === 'disabled',
  };
}

function ModelLifecyclePage(){
  var t = useToast();
  var _loading = useState(true); var loading = _loading[0], setLoading = _loading[1];
  var _models = useState([]); var models = _models[0], setModels = _models[1];
  var _stateFilter = useState('all'); var stateFilter = _stateFilter[0], setStateFilter = _stateFilter[1];
  var _providerFilter = useState('all'); var providerFilter = _providerFilter[0], setProviderFilter = _providerFilter[1];
  var _q = useState(''); var q = _q[0], setQ = _q[1];
  var _busyId = useState(null); var busyId = _busyId[0], setBusyId = _busyId[1];

  var load = function(){
    NR_API.discoveredModels().then(function(res){
      setModels((res && res.models) || []);
      setLoading(false);
    }).catch(function(){ setLoading(false); });
  };
  useEffect(load, []);

  var stateCounts = {};
  models.forEach(function(m){ stateCounts[m.activation_state] = (stateCounts[m.activation_state]||0) + 1; });
  var providerCounts = {};
  models.forEach(function(m){ providerCounts[m.provider] = (providerCounts[m.provider]||0) + 1; });
  var visibleProviders = Object.keys(providerCounts).sort();

  var filtered = models.filter(function(m){
    if(stateFilter !== 'all' && m.activation_state !== stateFilter) return false;
    if(providerFilter !== 'all' && m.provider !== providerFilter) return false;
    if(q){
      var needle = q.toLowerCase();
      var hay = (m.model_id||'').toLowerCase() + ' ' + (m.display_name||'').toLowerCase();
      if(hay.indexOf(needle) < 0) return false;
    }
    return true;
  });

  // runAction is the shared plumbing for all four mutations: mark the row
  // busy, call the API, and on success reload the list so every column
  // (activation_state, block_reason, last_seen_at, ...) reflects the write
  // immediately rather than trusting a locally-guessed next state.
  var runAction = function(fn, modelId, successMsg, opts){
    opts = opts || {};
    setBusyId(modelId);
    fn(modelId).then(function(res){
      setBusyId(null);
      if(res && res.ok){
        if(opts.onOk) opts.onOk(res.data);
        else t({title: successMsg});
        load();
      } else {
        t({kind:'error', title: opts.failTitle || 'Action failed', msg: (res && res.data && res.data.error) || ''});
      }
    }).catch(function(){
      setBusyId(null);
      t({kind:'error', title: opts.failTitle || 'Action failed'});
    });
  };

  var activate = function(id){ runAction(NR_API.activateModel, id, 'Model activated'); };
  var disable = function(id){
    if(!window.confirm('Disable '+id+'?\n\nIt stops routing immediately and is sticky — the sync and probe cycle will never bring it back on their own; only another admin action can.'))
      return;
    runAction(NR_API.disableModel, id, 'Model disabled');
  };
  var probe = function(id){
    runAction(NR_API.probeModel, id, null, {
      failTitle: 'Probe failed',
      onOk: function(data){
        t({title: 'Probe result: ' + (data.outcome || 'unknown'),
          msg: 'HTTP ' + data.status + (data.reason ? ' — ' + data.reason : '') + ' → ' + (data.activation_state || '')});
      }
    });
  };
  var decommission = function(id){
    if(!window.confirm('Decommission '+id+'?\n\nThis is the final retirement step. The row is kept (never deleted) for historical billing, but the model can never be reactivated after this.'))
      return;
    runAction(NR_API.decommissionModel, id, 'Model decommissioned (row retained)');
  };
  var recommission = function(id){
    // Not destructive (unlike disable/decommission) — it only clears the
    // sticky flag and lands the model on Ready. No confirm needed; the model
    // still isn't routable until a subsequent Activate call passes CanActivate.
    runAction(NR_API.recommissionModel, id, null, {
      failTitle: 'Recommission failed',
      onOk: function(){ t({title: 'Model recommissioned', msg: 'Back to Ready — click Activate to route to it again.'}); }
    });
  };

  if(loading) return <LoadingPage/>;

  return (
    <div className="grid" style={{gap:18}}>
      <PageHead desc="Every model's position in the discovery → activation → decommission lifecycle. Discovery runs weekly per provider; availability is re-probed hourly (faster while unavailable); a live request failure triggers an immediate probe. Activating, disabling, and decommissioning are always explicit admin actions — nothing here is automatic.">
        <button className="btn btn-soft btn-sm" onClick={load}><Icon name="refresh" size={14}/>Refresh</button>
      </PageHead>

      <div className="row" style={{gap:8,flexWrap:'wrap'}}>
        <button className={'chip'+(stateFilter==='all'?' chip-active':'')} onClick={function(){setStateFilter('all');}}>
          All <span className="faint">{models.length}</span>
        </button>
        {LIFECYCLE_STATES.map(function(s){
          var n = stateCounts[s.id] || 0;
          var active = stateFilter === s.id;
          return (
            <button key={s.id} className="chip" title={s.desc} onClick={function(){setStateFilter(s.id);}}
              style={active?{background:'rgba(255,255,255,.08)',color:'var(--tx-0)',borderColor:'var(--line-2)'}:null}>
              <span className="statusdot" style={{background:'var(--'+s.color+')'}}/>
              {s.label} <span className="faint">{n}</span>
            </button>
          );
        })}
      </div>

      <div className="row" style={{gap:8,flexWrap:'wrap',alignItems:'center'}}>
        {visibleProviders.length > 1 && <React.Fragment>
          <button className={'chip'+(providerFilter==='all'?' chip-active':'')} onClick={function(){setProviderFilter('all');}}>
            All providers
          </button>
          {visibleProviders.map(function(p){
            var info = DATA.providerById(p);
            var active = providerFilter === p;
            return (
              <button key={p} className="chip" onClick={function(){setProviderFilter(p);}}
                style={active?{borderColor:info.color,color:'var(--tx-0)',background:'rgba(255,255,255,.06)'}:null}>
                <span className="statusdot" style={{background:info.color}}/>{info.name} <span className="faint">{providerCounts[p]}</span>
              </button>
            );
          })}
        </React.Fragment>}
        <input className="input" style={{minWidth:220,marginLeft:'auto'}} placeholder="Search model id or name…"
          value={q} onChange={function(e){setQ(e.target.value);}}/>
      </div>

      {filtered.length===0 ?
        <EmptyState icon="layers" title={models.length===0 ? 'No models discovered yet' : 'No models match this filter'}
          desc={models.length===0
            ? 'The weekly catalog sync populates this list automatically once a provider key is configured (today: DeepInfra and Anthropic sync via a live list API; Vertex is discovered via its manifest + probe). Nothing to do here yet.'
            : 'Try a different state or provider filter, or clear the search box.'}/> :
        <SectionCard title="Model catalog" sub={filtered.length+' of '+models.length+' models'} pad={false}>
          <div className="scroll-x"><table className="table">
            <thead><tr>
              <th>Model</th><th>Provider</th><th>State</th><th>Pricing (per 1K)</th>
              <th>Capabilities</th><th>Source</th><th>Last seen</th><th></th>
            </tr></thead>
            <tbody>{filtered.map(function(m){
              var s = LIFECYCLE_STATE_BY_ID[m.activation_state] || {label: m.activation_state, color:'gray'};
              var acts = lifecycleActions(m.activation_state);
              var busy = busyId === m.model_id;
              var missingPrice = m.input_per_1k==null || m.output_per_1k==null;
              var missingCaps = m.max_context_tokens==null;
              var providerInfo = DATA.providerById(m.provider);
              return (
                <tr key={m.model_id}>
                  <td>
                    <div style={{fontWeight:600}}>{m.display_name || m.model_id}</div>
                    <div className="faint mono" style={{fontSize:11}}>{m.model_id}</div>
                  </td>
                  <td>
                    <span className="row" style={{gap:6,alignItems:'center'}}>
                      <span className="statusdot" style={{background:providerInfo.color}}/>{providerInfo.name}
                    </span>
                  </td>
                  <td title={s.desc}>
                    <Badge color={s.color}>{s.label}</Badge>
                    {m.block_reason && <div className="faint" style={{fontSize:11,marginTop:4,maxWidth:220}} title={m.block_reason}>{m.block_reason}</div>}
                    {m.missed_syncs > 0 && <div className="faint" style={{fontSize:11,marginTop:2}}>{m.missed_syncs} missed sync{m.missed_syncs===1?'':'s'}</div>}
                  </td>
                  <td className="num">
                    {missingPrice
                      ? <span className="faint">— no price yet</span>
                      : <span>${m.input_per_1k.toFixed(4)} / ${m.output_per_1k.toFixed(4)}</span>}
                    {m.price_source && <div className="faint" style={{fontSize:11}}>{m.price_source}</div>}
                  </td>
                  <td className="faint" style={{fontSize:12}}>
                    {missingCaps ? '—' : fmtNum(m.max_context_tokens)+' ctx'}
                    {m.supports_vision && <div>Vision</div>}
                    {m.supports_tools && <div>Tools</div>}
                  </td>
                  <td><span className="faint" style={{fontSize:12}}>{m.source}</span></td>
                  <td className="faint" style={{fontSize:12,whiteSpace:'nowrap'}}>{m.last_seen_at ? timeAgo(m.last_seen_at) : '—'}</td>
                  <td>
                    <div className="row" style={{gap:6,flexWrap:'wrap',justifyContent:'flex-end'}}>
                      {acts.probe && <button className="btn btn-soft btn-xs" disabled={busy} onClick={function(){probe(m.model_id);}}>Probe now</button>}
                      {acts.recommission && <button className="btn btn-soft btn-xs" disabled={busy} onClick={function(){recommission(m.model_id);}}>Recommission</button>}
                      {acts.activate && <button className="btn btn-primary btn-xs" disabled={busy} onClick={function(){activate(m.model_id);}}>Activate</button>}
                      {acts.disable && <button className="btn btn-ghost btn-xs" disabled={busy} onClick={function(){disable(m.model_id);}}>Disable</button>}
                      {acts.decommission && <button className="btn btn-danger btn-xs" disabled={busy} onClick={function(){decommission(m.model_id);}}>Decommission</button>}
                    </div>
                  </td>
                </tr>
              );
            })}</tbody>
          </table></div>
        </SectionCard>
      }
    </div>
  );
}

window.PAGES = Object.assign(window.PAGES||{}, {lifecycle: ModelLifecyclePage});
