/* global React, Ic, CloudGate */
const { useState, useMemo, useEffect, useRef } = React;

// =================== SHARED CRUD COMPONENTS ===================
// Generic entity table + drawer, plus simple LookupTable for {Id, Name} entities.

// Color palettes for chip/avatar accents (cycled by index)
const ACCENT_GRADS = [
  'linear-gradient(135deg, oklch(0.62 0.18 260), oklch(0.5 0.2 270))',
  'linear-gradient(135deg, oklch(0.6 0.18 200), oklch(0.5 0.2 220))',
  'linear-gradient(135deg, oklch(0.65 0.2 320), oklch(0.55 0.22 300))',
  'linear-gradient(135deg, oklch(0.6 0.2 155), oklch(0.5 0.2 165))',
  'linear-gradient(135deg, oklch(0.7 0.18 75), oklch(0.6 0.2 60))',
  'linear-gradient(135deg, oklch(0.55 0.18 285), oklch(0.45 0.2 270))',
];
function gradFor(seed) {
  const s = String(seed || '');
  let h = 0; for (let i=0;i<s.length;i++) h = (h*31 + s.charCodeAt(i)) >>> 0;
  return ACCENT_GRADS[h % ACCENT_GRADS.length];
}
function initialsOf(s) {
  return String(s||'?').split(/[\s_\-./]+/).filter(Boolean).slice(0,2).map(w=>w[0]).join('').toUpperCase() || '?';
}

// PageHeader — page title + actions
function PageHeader({ title, sub, onNew, newLabel = 'New', onExport, onImport, importLabel = 'Import' }) {
  return (
    <div className="page-header">
      <div>
        <h1 className="page-title">{title}</h1>
        {sub && <p className="page-sub">{sub}</p>}
      </div>
      <div className="page-actions">
        {onExport && <button className="btn" onClick={onExport}><Ic.download className="icon"/>Export</button>}
        {onImport && <button className="btn" onClick={onImport}><Ic.upload className="icon"/>{importLabel}</button>}
        {onNew && <button className="btn primary" onClick={onNew}><Ic.plus className="icon"/>{newLabel}</button>}
      </div>
    </div>
  );
}

// CrudToolbar — search + filters
function CrudToolbar({ q, onQ, placeholder, filters, count, total, children }) {
  return (
    <div className="toolbar">
      <div className="input-with-icon" style={{width:280}}>
        <Ic.search className="icon"/>
        <input className="input" placeholder={placeholder || 'Search…'} value={q} onChange={e => onQ(e.target.value)} style={{width:'100%'}}/>
      </div>
      {filters}
      {children}
      <div className="spacer"/>
      <span style={{fontSize:12, color:'var(--text-muted)'}}>{count} of {total}</span>
    </div>
  );
}

// RowActions — edit + delete buttons (right aligned)
// Delete always shows a confirmation prompt before invoking the parent's
// onDelete.  Caller can pass `confirmMessage={null}` to opt out (e.g. when
// the parent already prompts with a more specific message).
function RowActions({ onEdit, onDelete, confirmMessage = 'Delete this record? This cannot be undone.' }) {
  const handleDelete = (e) => {
    e.stopPropagation();
    if (!onDelete) return;
    if (confirmMessage && !window.confirm(confirmMessage)) return;
    onDelete();
  };
  return (
    <div style={{display:'flex', gap:4, justifyContent:'flex-end'}}>
      {onEdit && <button className="icon-btn" title="Edit" onClick={(e) => { e.stopPropagation(); onEdit(); }}><Ic.edit width="14" height="14"/></button>}
      {onDelete && <button className="icon-btn" title="Delete" onClick={handleDelete}><Ic.trash width="14" height="14"/></button>}
    </div>
  );
}

// Audit timestamps cell — Modified only (Created omitted by design;
// admins who want it can read it from the row's drawer).
function AuditCell({ modified }) {
  return (
    <div style={{fontSize:12, color:'var(--text-muted)'}}>
      {CloudGate.relTime(modified)}
    </div>
  );
}

// Badge for type / status pills
function Pill({ tone = 'default', children }) {
  const tones = {
    default: { bg: 'oklch(0.95 0.02 280)', fg: 'oklch(0.4 0.05 280)' },
    brand:   { bg: 'var(--brand-soft)',    fg: 'var(--brand-soft-text)' },
    ok:      { bg: 'var(--ok-soft)',       fg: 'oklch(0.38 0.18 155)' },
    warn:    { bg: 'var(--warn-soft)',     fg: 'oklch(0.45 0.2 60)' },
    err:     { bg: 'var(--err-soft)',      fg: 'oklch(0.45 0.22 25)' },
    info:    { bg: 'var(--info-soft)',     fg: 'oklch(0.42 0.18 230)' },
    cyan:    { bg: 'oklch(0.94 0.06 200)', fg: 'oklch(0.4 0.18 205)' },
    pink:    { bg: 'oklch(0.95 0.06 0)',   fg: 'oklch(0.45 0.22 0)' },
  };
  const t = tones[tone] || tones.default;
  return (
    <span style={{display:'inline-flex', alignItems:'center', gap:5, padding:'3px 9px', borderRadius:99,
      background: t.bg, color: t.fg, fontSize:11, fontWeight:600, textTransform:'uppercase', letterSpacing:'0.04em'}}>
      <span style={{width:6, height:6, borderRadius:'50%', background:'currentColor'}}/>
      {children}
    </span>
  );
}

// Drawer shell
function Drawer({ title, subtitle, onClose, onSave, saveLabel, children, wide }) {
  // Save click first flushes any pending UDF values registered by a child
  // CustomFieldsSection, then forwards to the parent's onSave.  Cancel does
  // not flush, so unsaved UDF edits are discarded — matching the user's
  // expectation that values only persist when "Save" is clicked.
  const handleSave = async () => {
    if (!onSave) return;
    const flushers = window.__udfFlushers ? Object.values(window.__udfFlushers) : [];
    if (flushers.length > 0) {
      try { await Promise.all(flushers.map(f => f())); }
      catch (err) { console.warn('[Drawer] UDF flush failed', err); }
    }
    onSave();
  };
  return (
    <>
      <div className="drawer-overlay" onClick={onClose}/>
      <aside className="drawer" style={wide ? { width: 540 } : undefined}>
        <div className="drawer-header">
          <div>
            <div style={{fontSize:11, color:'var(--text-muted)', textTransform:'uppercase', letterSpacing:'0.05em', fontWeight:600}}>{subtitle}</div>
            <div style={{fontSize:16, fontWeight:600, marginTop:2}}>{title}</div>
          </div>
          <button className="icon-btn" onClick={onClose}><Ic.x width="16" height="16"/></button>
        </div>
        <div className="drawer-body">
          <div style={{display:'flex', flexDirection:'column', gap:14}}>{children}</div>
        </div>
        <div style={{padding:14, borderTop:'1px solid var(--border)', display:'flex', justifyContent:'flex-end', gap:8}}>
          <button className="btn" onClick={onClose}>Cancel</button>
          {onSave && <button className="btn primary" onClick={handleSave}>{saveLabel || 'Save'}</button>}
        </div>
      </aside>
    </>
  );
}

// Generic field controls
function TextField({ label, value, onChange, placeholder, hint, type = 'text', required, maxLength, autoComplete, noAutofill }) {
  const [show, setShow] = useState(false);
  const isPw = type === 'password';
  // ── Never emit a real type="password" ─────────────────────────────────
  // A type=password input is what makes the browser / password manager treat
  // the whole drawer as a LOGIN form and autofill a saved credential into the
  // username box (which silently renamed Admin → Viewer2 — even with readOnly
  // and every documented opt-out attribute, because aggressive managers inject
  // the value programmatically).  So password fields render as a normal text
  // input masked with CSS (-webkit-text-security); the eye toggle swaps the
  // mask.  No password input ⇒ no login-form detection ⇒ no autofill.
  const inputType = isPw ? 'text' : type;
  const masked = isPw && !show;
  // ── Autofill suppression ──────────────────────────────────────────────
  // Password boxes always get it; other fields opt in via `noAutofill` (e.g.
  // the admin "edit user" Username/Email).  `autocomplete` alone is widely
  // ignored by browsers AND password managers, which is how a saved login
  // (e.g. Viewer2 / Admin#1234) got silently filled into the edit form and
  // overwrote the user's name on save.  So we ALSO emit each major manager's
  // documented opt-out attribute so they don't offer to fill these fields.
  const guard = noAutofill || isPw;
  // The one autofill block that works against EVERY browser & password manager:
  // a readOnly field is never autofilled.  So guarded fields start readOnly and
  // unlock on focus — editing is unaffected (clicking focuses the field, which
  // unlocks it before the first keystroke), but a load-time autofill (which is
  // how a saved login got injected, renaming Admin → Viewer2) can't touch it.
  // The data-*/autocomplete hints below additionally stop the managers that
  // honour them from even showing their fill icon.
  const [ro, setRo] = useState(guard);
  const ac = autoComplete ?? (isPw ? 'new-password' : (guard ? 'off' : undefined));
  const guardProps = guard ? {
    readOnly:         ro,
    onFocus:          () => { if (ro) setRo(false); },
    'data-lpignore':  'true',    // LastPass
    'data-1p-ignore': 'true',    // 1Password
    'data-bwignore':  'true',    // Bitwarden
    'data-form-type': 'other',   // Dashlane
    autoCorrect:      'off',
    spellCheck:       false,
  } : {};
  return (
    <div className="field">
      <label>{label}{required && <span style={{color:'var(--err)'}}> *</span>}</label>
      {isPw ? (
        // Flex wrapper so the input fills the field width; the eye is overlaid
        // absolutely on the input's right edge (inside the box).
        <div style={{position:'relative', display:'flex'}}>
          <input className="input" type={inputType} value={value ?? ''}
                 onChange={e => onChange(e.target.value)}
                 placeholder={placeholder} maxLength={maxLength} autoComplete={ac} {...guardProps}
                 style={{flex:1, minWidth:0, paddingRight:32, boxSizing:'border-box', WebkitTextSecurity: masked ? 'disc' : 'none'}}/>
          <button type="button" className="icon-btn pw-toggle"
                  title={show ? 'Hide password' : 'Show password'}
                  aria-label={show ? 'Hide password' : 'Show password'}
                  onClick={() => setShow(s => !s)}
                  style={{position:'absolute', right:2, top:'50%', transform:'translateY(-50%)', width:28, height:28}}>
            {show ? <Ic.eyeOff width="15" height="15"/> : <Ic.eye width="15" height="15"/>}
          </button>
        </div>
      ) : (
        <input className="input" type={inputType} value={value ?? ''} onChange={e => onChange(type==='number' ? +e.target.value : e.target.value)} placeholder={placeholder} maxLength={maxLength} autoComplete={ac} {...guardProps}/>
      )}
      {hint && <span className="hint">{hint}</span>}
    </div>
  );
}
function SelectField({ label, value, onChange, options, hint, required, placeholder }) {
  return (
    <div className="field">
      <label>{label}{required && <span style={{color:'var(--err)'}}> *</span>}</label>
      <select className="select" value={value ?? ''} onChange={e => onChange(isNaN(+e.target.value) || e.target.value === '' ? e.target.value : +e.target.value)}>
        {placeholder && <option value="">{placeholder}</option>}
        {options.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
      </select>
      {hint && <span className="hint">{hint}</span>}
    </div>
  );
}
function TextAreaField({ label, value, onChange, hint, rows = 4 }) {
  return (
    <div className="field">
      <label>{label}</label>
      <textarea className="input" style={{height: rows*22, padding:8, resize:'vertical', fontFamily: 'var(--mono)', fontSize:12}} value={value || ''} onChange={e => onChange(e.target.value)}/>
      {hint && <span className="hint">{hint}</span>}
    </div>
  );
}
function FieldRow({ children }) {
  return <div style={{display:'grid', gridTemplateColumns: `repeat(${React.Children.count(children)}, 1fr)`, gap:12}}>{children}</div>;
}

// Custom-fields section in drawer.
// Lets the user (a) enter values for already-defined custom fields on the
// current record, AND (b) add / rename / retype / remove field DEFINITIONS
// inline — definitions are scoped per EntityName and persist to CloudGate.DB.CustomFields.
// When `entityId` is provided, value edits are persisted to the database via
// POST /api/udf/value (debounced 400 ms).  Without an entityId (e.g. a brand-
// new unsaved row) edits stay in browser state until the row gets an Id.
function CustomFieldsSection({ entityName, entityId, values, onValuesChange }) {
  // local mirror of the definition list so this drawer reflects edits live
  const [defs, setDefs] = useState(() => CloudGate.DB.CustomFields.filter(f => f.EntityName === entityName));
  // Initial values: caller-supplied → else stored row.FieldValues from snapshot.
  // Map entity-name to its plural snapshot key (Company → Companies, not Companys).
  const TABLE_KEY = {
    LocationType: 'LocationTypes',
    Location:     'Locations',
    CompanyType:  'CompanyTypes',
    Company:      'Companies',
    User:         'Users',
    Asset:        'Assets',
    Reader:       'Readers',
  };
  const initialVals = (() => {
    if (values) return values;
    if (entityId == null) return {};
    const tableKey = TABLE_KEY[entityName];
    const table = (CloudGate.DB[tableKey] || []);
    const row = table.find(r => r.Id === entityId);
    return (row && row.FieldValues) ? { ...row.FieldValues } : {};
  })();
  const [vals, setVals] = useState(initialVals);
  const [adding, setAdding] = useState(false);
  const [editingId, setEditingId] = useState(null);

  const persist = (next) => {
    setDefs(next);
    const others = CloudGate.DB.CustomFields.filter(f => f.EntityName !== entityName);
    CloudGate.DB.CustomFields = [...others, ...next];
  };

  // Persist a new field DEFINITION to the database.  We must use the
  // server-returned Id (NOT a client-generated one) because that Id is the FK
  // value-rows reference — making it up locally produces orphan FieldDefinitionIds.
  const addDef = async (def) => {
    try {
      const res = await fetch('/api/udf/definition', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body:    JSON.stringify({
          EntityName: entityName,
          FieldName:  def.FieldName,
          FieldType:  def.FieldType
        })
      });
      if (!res.ok) {
        const text = await res.text();
        console.warn('[UDF DEF] create failed', res.status, text);
        alert('Could not save the field: ' + text);
        return;
      }
      const created = await res.json();   // { Id, EntityName, FieldName, FieldType }
      console.log('[UDF DEF] created', created);
      persist([...defs, created]);
    } catch (err) {
      console.warn('[UDF DEF] create error', err);
    } finally {
      setAdding(false);
    }
  };

  const updateDef = async (id, patch) => {
    const current = defs.find(d => d.Id === id);
    if (!current) return;
    const merged = { ...current, ...patch };
    try {
      const res = await fetch('/api/udf/definition/' + id, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body:    JSON.stringify({
          EntityName: entityName,
          FieldName:  merged.FieldName,
          FieldType:  merged.FieldType
        })
      });
      if (!res.ok) {
        const text = await res.text();
        console.warn('[UDF DEF] update failed', res.status, text);
        return;
      }
      console.log('[UDF DEF] updated id=' + id);
      persist(defs.map(d => d.Id === id ? merged : d));
    } catch (err) {
      console.warn('[UDF DEF] update error', err);
    } finally {
      setEditingId(null);
    }
  };

  const removeDef = async (id) => {
    if (!confirm('Remove this custom field for all ' + entityName + ' records?')) return;
    try {
      const res = await fetch('/api/udf/definition/' + id, { method: 'DELETE' });
      if (!res.ok && res.status !== 404) {
        console.warn('[UDF DEF] delete failed', res.status);
        return;
      }
      console.log('[UDF DEF] deleted id=' + id);
      persist(defs.filter(d => d.Id !== id));
      const next = { ...vals }; delete next[id];
      setVals(next); onValuesChange && onValuesChange(next);
    } catch (err) {
      console.warn('[UDF DEF] delete error', err);
    }
  };

  // POST one value to the server, then update the in-memory snapshot row.
  const saveValue = async (fieldDefId, raw) => {
    if (entityId == null) { console.warn('[UDF] no entityId — skip save'); return; }
    const body = {
      FieldDefinitionId: fieldDefId,
      EntityId:          entityId,
      Value:             (raw === '' || raw == null) ? null : String(raw)
    };
    console.log('[UDF] POST', body);
    try {
      const res = await fetch('/api/udf/value', {
        method:  'POST',
        headers: { 'Content-Type': 'application/json' },
        body:    JSON.stringify(body)
      });
      if (!res.ok) {
        const text = await res.text();
        console.warn('[UDF] save failed', res.status, text);
        return;
      }
      console.log('[UDF] saved');
      const row = (CloudGate.DB[TABLE_KEY[entityName]] || []).find(r => r.Id === entityId);
      if (row) {
        if (raw === '' || raw == null) {
          if (row.FieldValues) delete row.FieldValues[fieldDefId];
        } else {
          row.FieldValues = { ...(row.FieldValues || {}), [fieldDefId]: raw };
        }
      }
    } catch (err) {
      console.warn('[UDF] save error', err);
    }
  };
  // Pending edits — flushed only when the Drawer's Save button is clicked.
  // Cancel discards them.  Each entry: fieldDefId → latest raw value.
  const pendingRef = useRef({});

  const setVal = (id, v) => {
    const next = { ...vals, [id]: v };
    setVals(next);
    onValuesChange && onValuesChange(next);
    pendingRef.current[id] = v;   // mark dirty; Drawer.handleSave will flush

    // For a brand-new (unsaved) row we have no entityId yet, so saveValue
    // can't POST.  Mirror the latest values onto a window-scoped bucket
    // keyed by entityName — the parent's save flow drains it (via
    // CloudGate.flushNewUdfs) after the row is created and gets its Id.
    if (entityId == null) {
      if (!window.__udfPendingNew) window.__udfPendingNew = {};
      window.__udfPendingNew[entityName] = { ...pendingRef.current };
    }
  };

  // Register a flush function on a window-level registry while mounted, so
  // Drawer.handleSave can await it before calling the parent's onSave.
  useEffect(() => {
    if (entityId == null) {
      // Brand-new row — no flusher to register, but make sure we don't leak
      // stale "pending new" values from a previously cancelled drawer into
      // the next one for the same entity.
      return () => {
        if (window.__udfPendingNew) delete window.__udfPendingNew[entityName];
      };
    }
    if (!window.__udfFlushers) window.__udfFlushers = {};
    const key = entityName + ':' + entityId;
    window.__udfFlushers[key] = async () => {
      const items = Object.entries(pendingRef.current);
      if (items.length === 0) return;
      console.log('[UDF] flushing', items.length, 'pending value(s) for', key);
      for (const [fieldDefIdStr, raw] of items) {
        await saveValue(parseInt(fieldDefIdStr, 10), raw);
      }
      pendingRef.current = {};
    };
    return () => { delete window.__udfFlushers[key]; };
  }, [entityName, entityId]);

  const renderInput = (d) => {
    const v = vals[d.Id] ?? '';
    switch (d.FieldType) {
      case 4: return <select className="select" value={v} onChange={e => setVal(d.Id, e.target.value)}><option value="">—</option><option value="true">true</option><option value="false">false</option></select>;
      case 3: return <input className="input" type="datetime-local" value={v} onChange={e => setVal(d.Id, e.target.value)}/>;
      case 1:
      case 2: return <input className="input" type="number" value={v} onChange={e => setVal(d.Id, e.target.value)}/>;
      case 5: return <button className="btn" style={{alignSelf:'flex-start'}} type="button"><Ic.upload width="14" height="14"/>Upload image</button>;
      default: return <input className="input" value={v} onChange={e => setVal(d.Id, e.target.value)}/>;
    }
  };

  return (
    <div style={{padding:12, border:'1px dashed var(--border-strong)', borderRadius:6, background:'var(--surface-2)'}}>
      <div style={{display:'flex', alignItems:'center', justifyContent:'space-between', marginBottom:10}}>
        <div style={{display:'flex', alignItems:'baseline', gap:8}}>
          <span style={{fontSize:11, color:'var(--text-muted)', textTransform:'uppercase', letterSpacing:'0.05em', fontWeight:700}}>Custom fields</span>
          <span style={{fontSize:11, color:'var(--text-faint)'}}>· {entityName}</span>
        </div>
        {!adding && <button type="button" className="btn sm" onClick={() => setAdding(true)}><Ic.plus width="12" height="12"/>Add field</button>}
      </div>

      {defs.length === 0 && !adding && (
        <div style={{fontSize:12, color:'var(--text-muted)', padding:'8px 0', fontStyle:'italic'}}>
          No custom fields defined for {entityName}. Click <strong style={{fontStyle:'normal'}}>Add field</strong> to create one — it will be available on every {entityName} record.
        </div>
      )}

      <div style={{display:'flex', flexDirection:'column', gap:10}}>
        {defs.map(d => editingId === d.Id ? (
          <CustomFieldDefRow key={d.Id} def={d} onSave={(patch) => updateDef(d.Id, patch)} onCancel={() => setEditingId(null)}/>
        ) : (
          <div key={d.Id} className="field" style={{position:'relative'}}>
            <div style={{display:'flex', alignItems:'center', justifyContent:'space-between'}}>
              <label style={{margin:0}}>
                {d.FieldName}
                <span style={{color:'var(--text-faint)', fontWeight:400}}> · {CloudGate.FIELD_TYPE_LABELS[d.FieldType]}</span>
              </label>
              <div style={{display:'flex', gap:2}}>
                <button type="button" className="icon-btn" title="Edit definition" onClick={() => setEditingId(d.Id)}><Ic.edit width="12" height="12"/></button>
                <button type="button" className="icon-btn" title="Delete field" onClick={() => removeDef(d.Id)}><Ic.trash width="12" height="12"/></button>
              </div>
            </div>
            {renderInput(d)}
          </div>
        ))}

        {adding && (
          <CustomFieldDefRow onSave={addDef} onCancel={() => setAdding(false)} isNew/>
        )}
      </div>
    </div>
  );
}

// Inline editor for a single custom-field DEFINITION (name + type)
function CustomFieldDefRow({ def, onSave, onCancel, isNew }) {
  const [name, setName] = useState(def?.FieldName || '');
  const [type, setType] = useState(def?.FieldType ?? 0);
  const submit = () => {
    if (!name.trim()) return;
    onSave({ FieldName: name.trim(), FieldType: +type });
  };
  return (
    <div style={{padding:10, border:'1px solid var(--brand)', borderRadius:6, background:'var(--surface)', display:'flex', flexDirection:'column', gap:8}}>
      <div style={{fontSize:11, color:'var(--brand-soft-text)', textTransform:'uppercase', letterSpacing:'0.05em', fontWeight:700}}>
        {isNew ? 'New custom field' : 'Edit field definition'}
      </div>
      <div style={{display:'grid', gridTemplateColumns:'1fr 140px', gap:8}}>
        <input className="input" placeholder="Field name (e.g. VAT number)" value={name} onChange={e => setName(e.target.value)} autoFocus/>
        <select className="select" value={type} onChange={e => setType(+e.target.value)}>
          {CloudGate.FIELD_TYPE_LABELS.map((l, i) => <option key={i} value={i}>{l}</option>)}
        </select>
      </div>
      <div style={{display:'flex', justifyContent:'flex-end', gap:6}}>
        <button type="button" className="btn sm" onClick={onCancel}>Cancel</button>
        <button type="button" className="btn sm primary" onClick={submit}>{isNew ? 'Add field' : 'Save'}</button>
      </div>
    </div>
  );
}

// ────────────────────────────────────────────────────────────────────
// LookupTable — for simple {Id, <NameField>} tables (UserLevels, ReaderTypes, etc)
// ────────────────────────────────────────────────────────────────────
function LookupManager({ data, nameField, label, sub, icon, onChange, immutable, entity, dbKey }) {
  const [rows, setRows] = useState(data);
  const [q, setQ] = useState('');
  const [editing, setEditing] = useState(null);
  const [showNew, setShowNew] = useState(false);

  const filtered = rows.filter(r => !q || String(r[nameField]).toLowerCase().includes(q.toLowerCase()));

  // Persist to DB if `entity` (URL segment) is supplied; fall back to local-only.
  const save = async (row) => {
    if (entity) {
      const saved = await CloudGate.apiSave(entity, row);
      if (!saved) return;
      setRows(rs => {
        const next = row.Id ? rs.map(r => r.Id === saved.Id ? saved : r) : [...rs, saved];
        onChange && onChange(next);
        return next;
      });
      if (dbKey) {
        CloudGate.DB[dbKey] = row.Id
          ? CloudGate.DB[dbKey].map(r => r.Id === saved.Id ? saved : r)
          : [...CloudGate.DB[dbKey], saved];
      }
    } else {
      setRows(rs => {
        const next = row.Id ? rs.map(r => r.Id === row.Id ? row : r) : [...rs, { ...row, Id: Math.max(0, ...rs.map(r => r.Id)) + 1, Created: new Date().toISOString(), Modified: new Date().toISOString() }];
        onChange && onChange(next);
        return next;
      });
    }
    setEditing(null); setShowNew(false);
  };
  const remove = async (id) => {
    if (entity) {
      if (!await CloudGate.apiDelete(entity, id)) return;
    }
    setRows(rs => {
      const next = rs.filter(r => r.Id !== id);
      onChange && onChange(next);
      return next;
    });
    if (dbKey && CloudGate.DB[dbKey]) CloudGate.DB[dbKey] = CloudGate.DB[dbKey].filter(r => r.Id !== id);
  };

  return (
    <div className="card">
      <div className="card-header" style={{display:'flex', justifyContent:'space-between', alignItems:'center'}}>
        <div style={{display:'flex', gap:10, alignItems:'center'}}>
          {icon && <div style={{width:28, height:28, borderRadius:6, background:'var(--brand-grad)', display:'flex', alignItems:'center', justifyContent:'center', color:'white'}}>{icon}</div>}
          <div>
            <div className="card-title">{label}</div>
            {sub && <div style={{fontSize:11.5, color:'var(--text-muted)', marginTop:2}}>{sub}</div>}
          </div>
        </div>
        {!immutable && <button className="btn sm" onClick={() => setShowNew(true)}><Ic.plus width="12" height="12"/>Add</button>}
      </div>
      <div style={{padding:'10px 14px', borderBottom:'1px solid var(--border)', display:'flex', gap:8, alignItems:'center'}}>
        <div className="input-with-icon" style={{flex:1}}>
          <Ic.search className="icon"/>
          <input className="input" placeholder="Search…" value={q} onChange={e => setQ(e.target.value)} style={{width:'100%'}}/>
        </div>
        <span style={{fontSize:11.5, color:'var(--text-muted)'}}>{filtered.length}/{rows.length}</span>
      </div>
      <table className="tbl">
        <thead>
          <tr>
            {CloudGate.isAdmin() && <th style={{width:50}}>#</th>}
            <th>{nameField}</th>
            {CloudGate.isAdmin() && <th style={{width:120}}>Modified</th>}
            <th style={{width:80}}></th>
          </tr>
        </thead>
        <tbody>
          {filtered.map(r => (
            <tr key={r.Id}>
              {CloudGate.isAdmin() && <td style={{color:'var(--text-faint)', fontFamily:'var(--mono)', fontSize:12}}>{r.Id}</td>}
              <td style={{fontWeight:600}}>{r[nameField]}</td>
              {CloudGate.isAdmin() && <td style={{color:'var(--text-muted)', fontSize:12}}>{CloudGate.relTime(r.Modified)}</td>}
              <td><RowActions
                onEdit={!immutable ? () => setEditing(r) : null}
                onDelete={!immutable ? () => remove(r.Id) : null}
              /></td>
            </tr>
          ))}
          {filtered.length === 0 && <tr><td colSpan={CloudGate.isAdmin() ? 4 : 2} style={{textAlign:'center', padding:'24px 12px', color:'var(--text-muted)', fontSize:12}}>No items</td></tr>}
        </tbody>
      </table>

      {(editing || showNew) && (
        <LookupDrawer
          row={editing}
          nameField={nameField}
          label={label}
          onClose={() => { setEditing(null); setShowNew(false); }}
          onSave={save}
        />
      )}
    </div>
  );
}

function LookupDrawer({ row, nameField, label, onClose, onSave }) {
  const [form, setForm] = useState(row || { [nameField]: '' });
  return (
    <Drawer
      title={row ? form[nameField] || 'Untitled' : `New ${label.replace(/s$/,'').toLowerCase()}`}
      subtitle={row ? `Edit ${label.toLowerCase().replace(/s$/,'')}` : `Create ${label.toLowerCase().replace(/s$/,'')}`}
      onClose={onClose}
      onSave={() => onSave(form)}
      saveLabel={row ? 'Save changes' : 'Create'}>
      <TextField label={nameField} value={form[nameField]} onChange={v => setForm({ ...form, [nameField]: v })} required hint={`Identifier for this ${label.toLowerCase().replace(/s$/,'')}.`}/>
      {row && CloudGate.isAdmin() && (
        <div style={{padding:10, background:'var(--surface-2)', border:'1px solid var(--border)', borderRadius:6, fontSize:11.5, color:'var(--text-muted)'}}>
          Modified · {CloudGate.fmtDate(row.Modified)}
        </div>
      )}
    </Drawer>
  );
}

window.PageHeader = PageHeader;
window.CrudToolbar = CrudToolbar;
window.RowActions = RowActions;
window.AuditCell = AuditCell;
window.Pill = Pill;
window.Drawer = Drawer;
window.TextField = TextField;
window.SelectField = SelectField;
window.TextAreaField = TextAreaField;
window.FieldRow = FieldRow;
window.CustomFieldsSection = CustomFieldsSection;
window.LookupManager = LookupManager;
window.gradFor = gradFor;
window.initialsOf = initialsOf;
