// /admin.html → CATALOG tab. Full product CRUD against D1.
//
// Two views, toggled by local state:
//   - LIST: table of every product, "+ NEW" button, click to edit
//   - EDITOR: form for one product, with image manager + features editor
//
// Every text/number field autosaves on blur (PATCH /api/admin/products/:id).
// Images and features have dedicated endpoints; both run instantly so the
// catalog reflects changes within ExecCatalog's next refresh window.

(function () {
  const { useState, useEffect, useMemo, useCallback, useRef } = React;

  // Category list is D1-backed (see js/categories.js). Components that
  // render category chips should call useCategoryList() so they re-render
  // when admin edits / adds / deletes a category.
  function useCategoryList() {
    window.ExecCategories && window.ExecCategories.useCategories();
    return (window.ExecCategories ? window.ExecCategories.list() : []);
  }

  function slugifyCategoryId(s) {
    return String(s || '')
      .toLowerCase()
      .replace(/[^a-z0-9-]/g, '-')
      .replace(/-+/g, '-')
      .replace(/^-+|-+$/g, '');
  }

  // ---------- root: list / editor / draft (create) ----------
  function AdminCatalogApp() {
    // Products are now managed in catalog.json (edit the file + `git push`).
    // This in-page editor is retired to avoid two conflicting sources of
    // truth: it used to write product rows to D1, which the live storefront
    // and checkout no longer read. The old ProductList/ProductEditor/etc.
    // components below are left defined but unused.
    window.ExecCatalog && window.ExecCatalog.useCatalog();
    const products = (window.ExecCatalog ? window.ExecCatalog.list() : []);
    return (
      <div style={{ padding: '24px', maxWidth: 760, margin: '0 auto', lineHeight: 1.6 }}>
        <h2 style={{ marginTop: 0 }}>Products are managed in <code>catalog.json</code></h2>
        <p>
          Product data lives in <code>catalog.json</code> in the GitHub repo. To add,
          edit, remove, or reprice a product, edit that file and <strong>push to{' '}
          <code>main</code></strong>. The storefront and the checkout both read it, so
          your change goes live automatically after Hostinger pulls the push.
        </p>
        <p style={{ opacity: 0.75 }}>
          This editor is retired. Editing here would write to the old database, which the
          live store no longer uses — the changes would not appear. Order &amp; key
          management is unaffected; use the Orders tab.
        </p>
        <p style={{ opacity: 0.6 }}>
          Currently live: {products.length} products (from <code>catalog.json</code>).
        </p>
      </div>
    );
  }

  function RestoredAdminCatalogApp() {
    window.ExecCatalog && window.ExecCatalog.useCatalog();
    const products = (window.ExecCatalog ? window.ExecCatalog.list() : []);
    const [view, setView] = useState('list');
    const [selectedId, setSelectedId] = useState(null);

    const refresh = useCallback(async () => {
      if (window.ExecCatalog) await window.ExecCatalog.fetchAll({ fresh: true });
    }, []);
    const backToList = () => {
      setSelectedId(null);
      setView('list');
    };
    const openEditor = (id) => {
      setSelectedId(id);
      setView('edit');
    };
    const onCreated = async (id) => {
      await refresh();
      setSelectedId(id);
      setView('edit');
    };

    const selected = products.find((p) => p.id === selectedId) || null;
    if (view === 'new') return <ProductDraftEditor onBack={backToList} onCreated={onCreated}/>;
    if (view === 'edit' && selected) {
      return <ProductEditor product={selected} onBack={backToList} onRefresh={refresh}/>;
    }
    return (
      <ProductList
        products={products}
        onEdit={openEditor}
        onRefresh={refresh}
        onNew={() => setView('new')}
      />
    );
  }

  // ---------- LIST VIEW ----------
  function ProductList({ products, onEdit, onRefresh, onNew }) {
    const [filter, setFilter] = useState('all');
    const [query, setQuery] = useState('');
    const categories = useCategoryList();

    const filtered = useMemo(() => {
      let arr = products;
      if (filter !== 'all') arr = arr.filter((p) => p.category === filter);
      if (query.trim()) {
        const q = query.trim().toLowerCase();
        arr = arr.filter((p) =>
          (p.name + ' ' + p.tag + ' ' + p.id + ' ' + p.blurb).toLowerCase().includes(q)
        );
      }
      return arr;
    }, [products, filter, query]);

    const counts = useMemo(() => {
      const out = { total: products.length, featured: 0 };
      for (const p of products) if (p.featured) out.featured++;
      for (const c of categories) out[c.id] = products.filter((p) => p.category === c.id).length;
      return out;
    }, [products, categories]);

    // The actual "open the modal" handler is owned by AdminCatalogApp; this
    // component just renders the trigger button.
    const createNew = () => onNew && onNew();

    return (
      <>
        <window.HeroStrip user={null} isAdmin={true}>
          <window.StatColumn label="TOTAL"      value={String(counts.total)}/>
          <window.StatColumn label="FEATURED"   value={String(counts.featured)}/>
          <window.StatColumn label="CATEGORIES" value={String(categories.length)}/>
          <window.StatColumn
            label="BY CATEGORY"
            value={categories.length
              ? categories.map((c) => `${c.label} ${counts[c.id] || 0}`).join(' · ')
              : '—'}
          />
        </window.HeroStrip>

        <div className="ap-sec-head">
          <div className="ap-sec-tag">// CATALOG</div>
          <h2 className="ap-sec-title">PRODUCTS <span className="ap-sec-count">/ {filtered.length}</span></h2>
          <div className="ap-sec-side">
            <ExportButton/>
            <button className="btn primary" onClick={createNew}><span>＋ NEW PRODUCT</span></button>
          </div>
        </div>

        <div className="adm-toolbar">
          <window.CornerCuts size={8}/>
          <label className="adm-search-wrap">
            <span className="adm-search-icon">⌕</span>
            <input
              className="adm-search"
              type="search"
              placeholder="search by name, tag, id, blurb"
              value={query}
              onChange={(e) => setQuery(e.target.value)}
            />
          </label>
          <div className="adm-filter">
            <span className="adm-filter-label">CATEGORY</span>
            <select className="adm-select" value={filter} onChange={(e) => setFilter(e.target.value)}>
              <option value="all">ALL · {counts.total}</option>
              {categories.map((c) => (
                <option key={c.id} value={c.id}>{c.label} · {counts[c.id] || 0}</option>
              ))}
            </select>
          </div>
        </div>

        <div className="adm-cat-list">
          {filtered.length === 0 && (
            <window.EmptyPanel art="[ ∅ ]" line="NO PRODUCTS" sub="adjust the filter or click + NEW PRODUCT"/>
          )}
          {filtered.map((p) => <ProductRow key={p.id} product={p} onEdit={onEdit}/>)}
        </div>
      </>
    );
  }

  // Downloads the live catalog snapshot to the admin's machine. The
  // GitHub Action handles the regular push-to-git backup every 15 min;
  // this button is for ad-hoc "I want a copy right now" moments.
  function ExportButton() {
    const [busy, setBusy] = useState(false);
    const onClick = async () => {
      setBusy(true);
      try {
        const res = await fetch('/api/admin/catalog/export', { credentials: 'same-origin' });
        if (!res.ok) { alert('Export failed: ' + res.status); return; }
        const blob = await res.blob();
        const url = URL.createObjectURL(blob);
        const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
        const a = document.createElement('a');
        a.href = url;
        a.download = `catalog-${stamp}.json`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
      } catch (e) { alert('Export failed: ' + e.message); }
      finally { setBusy(false); }
    };
    return (
      <button className="btn ghost" onClick={onClick} disabled={busy} title="Download a JSON snapshot of the live catalog">
        <span>{busy ? 'EXPORTING…' : '↓ EXPORT NOW'}</span>
      </button>
    );
  }

  function ProductRow({ product, onEdit }) {
    const cover = product.images && product.images[0];
    return (
      <article className="adm-cat-row" onClick={() => onEdit(product.id)}>
        <window.CornerCuts size={6}/>
        <div className="adm-cat-thumb">
          {cover ? <img src={cover} alt=""/> : <span className="adm-cat-thumb--ph">{(product.name[0] || '?')}</span>}
        </div>
        <div className="adm-cat-info">
          <div className="adm-cat-tag">{product.tag} · {(product.category || '').toUpperCase()}</div>
          <div className="adm-cat-name">{product.name} {product.featured && <span className="adm-cat-pin">★ FEATURED</span>}</div>
          <div className="adm-cat-blurb">{product.blurb}</div>
        </div>
        <InlineStatePicker productId={product.id}/>
        <div className="adm-cat-prices">
          <span>${product.pricing?.day || 0}/d</span>
          <span>${product.pricing?.week || 0}/w</span>
          <span className="big">${product.pricing?.month || 0}/mo</span>
        </div>
        <div className="adm-cat-edit">EDIT →</div>
      </article>
    );
  }

  // Inline operational-state picker. Lives on every catalog row so admin
  // can flip a product to OFFLINE / UPDATING without leaving CATALOG. The
  // click handlers stopPropagation so changing the dropdown doesn't also
  // open the editor row click.
  function InlineStatePicker({ productId }) {
    window.ExecStates && window.ExecStates.useStates();
    const cur = window.ExecStates ? window.ExecStates.get(productId) : { state: 'updating' };
    const [busy, setBusy] = useState(false);
    const onChange = async (e) => {
      e.stopPropagation();
      const next = e.target.value;
      if (next === cur.state) return;
      setBusy(true);
      try { await window.ExecStates.setState(productId, next, cur.note || ''); }
      catch { /* errors surface in console; pill won't lie */ }
      finally { setBusy(false); }
    };
    return (
      <div
        className={window.cls('adm-cat-state', 'adm-cat-state--' + cur.state, busy && 'busy')}
        onClick={(e) => e.stopPropagation()}
      >
        <span className="adm-cat-state-dot"/>
        <select
          className="adm-cat-state-select"
          value={cur.state}
          onChange={onChange}
          onClick={(e) => e.stopPropagation()}
          aria-label="Operational status"
        >
          <option value="online">ONLINE</option>
          <option value="updating">UPDATING</option>
          <option value="offline">OFFLINE</option>
        </select>
      </div>
    );
  }

  // ---------- CATEGORY PICKER (chip row + inline create + manage popover) ----------
  //
  // Shared by ProductDraftEditor (new product) and BasicFields (existing
  // product). Renders one chip per D1 category, plus a "＋ NEW" chip that
  // expands an inline create form, plus a tiny "✎" affordance on each chip
  // that opens a popover for rename / image upload / delete.
  //
  // Props:
  //   value:    selected category id (or null)
  //   onChange: (id) => void — called when the admin clicks a chip or after
  //             a brand-new category is created (auto-selects it)
  function CategoryPicker({ value, onChange }) {
    const categories = useCategoryList();
    const [creating, setCreating] = useState(false);
    const [managingId, setManagingId] = useState(null);
    const managing = managingId ? categories.find((c) => c.id === managingId) : null;

    return (
      <>
        <div className="adm-new-cats">
          {categories.map((c) => (
            <span key={c.id} className="adm-new-cat-wrap">
              <button
                type="button"
                className={window.cls('adm-new-cat', value === c.id && 'on')}
                onClick={() => onChange(c.id)}
              >{c.label}</button>
              <button
                type="button"
                className="adm-new-cat-edit"
                title={`Manage ${c.label}`}
                onClick={(e) => { e.stopPropagation(); setManagingId(c.id); }}
              >✎</button>
            </span>
          ))}
          {!creating && (
            <button
              type="button"
              className="adm-new-cat adm-new-cat--add"
              onClick={() => setCreating(true)}
              title="Create a new category"
            >＋ NEW</button>
          )}
        </div>
        {creating && (
          <CategoryCreateForm
            onCancel={() => setCreating(false)}
            onCreated={(newId) => { setCreating(false); onChange(newId); }}
          />
        )}
        {managing && (
          <CategoryManageModal
            category={managing}
            onClose={() => setManagingId(null)}
            onDeleted={(deletedId) => {
              setManagingId(null);
              if (value === deletedId) onChange(null);
            }}
          />
        )}
      </>
    );
  }

  function CategoryCreateForm({ onCancel, onCreated }) {
    const [label, setLabel] = useState('');
    const [idEdited, setIdEdited] = useState(false);
    const [idValue, setIdValue] = useState('');
    const [busy, setBusy] = useState(false);
    const [error, setError] = useState(null);

    useEffect(() => {
      if (idEdited) return;
      setIdValue(slugifyCategoryId(label));
    }, [label, idEdited]);

    const canSubmit = !busy && label.trim().length >= 2 && idValue.length >= 2;

    const submit = async () => {
      if (!canSubmit) return;
      setBusy(true); setError(null);
      try {
        const res = await fetch('/api/admin/categories', {
          method: 'POST',
          credentials: 'same-origin',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ id: idValue, label: label.trim().toUpperCase() }),
        });
        const data = await res.json().catch(() => ({}));
        if (!res.ok) throw new Error(data.error || ('http_' + res.status));
        await (window.ExecCategories && window.ExecCategories.fetchAll({ fresh: true }));
        onCreated(data.id || idValue);
      } catch (e) {
        setError(String(e.message || e));
        setBusy(false);
      }
    };

    return (
      <div className="adm-cat-create">
        <window.CornerCuts size={6}/>
        <div className="adm-cat-create-row">
          <label className="adm-cat-create-field">
            <span className="adm-cat-field-l">LABEL</span>
            <input
              type="text"
              className="adm-cat-input"
              value={label}
              onChange={(e) => setLabel(e.target.value)}
              placeholder="e.g. VALORANT"
              autoFocus
              disabled={busy}
            />
          </label>
          <label className="adm-cat-create-field">
            <span className="adm-cat-field-l">
              ID
              {idEdited && (
                <button
                  type="button"
                  className="adm-new-id-reset"
                  onClick={() => { setIdEdited(false); setIdValue(slugifyCategoryId(label)); }}
                >reset</button>
              )}
            </span>
            <input
              type="text"
              className="adm-cat-input"
              value={idValue}
              onChange={(e) => { setIdEdited(true); setIdValue(slugifyCategoryId(e.target.value)); }}
              placeholder="auto from label"
              disabled={busy}
            />
          </label>
          <div className="adm-cat-create-actions">
            <button type="button" className="btn ghost" onClick={onCancel} disabled={busy}>
              <span>CANCEL</span>
            </button>
            <button type="button" className="btn primary" onClick={submit} disabled={!canSubmit}>
              <span>{busy ? 'CREATING…' : '＋ CREATE CATEGORY'}</span>
            </button>
          </div>
        </div>
        {error && <div className="adm-cat-create-err">! {error}</div>}
        <div className="adm-cat-create-hint">
          Image banner is optional — upload one later via the ✎ button on the new chip.
        </div>
      </div>
    );
  }

  function CategoryManageModal({ category, onClose, onDeleted }) {
    const [label, setLabel] = useState(category.label);
    const [busy, setBusy] = useState(null);  // null | 'rename' | 'upload' | 'delete'
    const [error, setError] = useState(null);
    const [savedAt, setSavedAt] = useState(null);
    const fileRef = useRef(null);

    const renameSave = async () => {
      const next = label.trim();
      if (!next || next === category.label) return;
      setBusy('rename'); setError(null);
      try {
        const res = await fetch(`/api/admin/categories/${encodeURIComponent(category.id)}`, {
          method: 'PATCH',
          credentials: 'same-origin',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ label: next.toUpperCase() }),
        });
        const data = await res.json().catch(() => ({}));
        if (!res.ok) throw new Error(data.error || ('http_' + res.status));
        await (window.ExecCategories && window.ExecCategories.fetchAll({ fresh: true }));
        setSavedAt(Date.now());
      } catch (e) { setError(String(e.message || e)); }
      finally { setBusy(null); }
    };

    const uploadImage = async (file) => {
      if (!file || !file.type.startsWith('image/')) return;
      setBusy('upload'); setError(null);
      try {
        const res = await fetch(`/api/admin/categories/${encodeURIComponent(category.id)}/image`, {
          method: 'POST',
          credentials: 'same-origin',
          headers: { 'Content-Type': file.type },
          body: file,
        });
        const data = await res.json().catch(() => ({}));
        if (!res.ok) throw new Error(data.error || ('http_' + res.status));
        await (window.ExecCategories && window.ExecCategories.fetchAll({ fresh: true }));
        setSavedAt(Date.now());
      } catch (e) { setError(String(e.message || e)); }
      finally { setBusy(null); }
    };

    const doDelete = async () => {
      if (!window.confirm(`Delete category "${category.label}"?\n\nThis can't be undone. Categories with products attached will refuse to delete — reassign or delete those products first.`)) return;
      setBusy('delete'); setError(null);
      try {
        const res = await fetch(`/api/admin/categories/${encodeURIComponent(category.id)}`, {
          method: 'DELETE',
          credentials: 'same-origin',
        });
        const data = await res.json().catch(() => ({}));
        if (res.status === 409 && data && data.error === 'category_in_use') {
          throw new Error(`${data.count || 'some'} products still use this category. Reassign or delete them first.`);
        }
        if (!res.ok) throw new Error(data.error || ('http_' + res.status));
        await (window.ExecCategories && window.ExecCategories.fetchAll({ fresh: true }));
        onDeleted(category.id);
      } catch (e) { setError(String(e.message || e)); setBusy(null); }
    };

    return (
      <div className="adm-cat-modal-bg" onClick={onClose}>
        <div className="adm-cat-modal" onClick={(e) => e.stopPropagation()}>
          <window.CornerCuts size={8}/>
          <div className="adm-cat-modal-h">
            <span>MANAGE CATEGORY</span>
            <button className="adm-cat-modal-x" onClick={onClose} aria-label="Close">✕</button>
          </div>
          <div className="adm-cat-modal-body">
            <div className="adm-cat-modal-id">/ {category.id}</div>

            <label className="adm-cat-field">
              <span className="adm-cat-field-l">LABEL</span>
              <div className="adm-cat-modal-row">
                <input
                  type="text"
                  className="adm-cat-input"
                  value={label}
                  onChange={(e) => setLabel(e.target.value)}
                  disabled={busy === 'rename'}
                />
                <button
                  className="btn ghost"
                  onClick={renameSave}
                  disabled={busy === 'rename' || !label.trim() || label.trim() === category.label}
                ><span>{busy === 'rename' ? 'SAVING…' : 'RENAME'}</span></button>
              </div>
            </label>

            <label className="adm-cat-field">
              <span className="adm-cat-field-l">BANNER IMAGE</span>
              <div className="adm-cat-modal-row">
                <div className="adm-cat-modal-thumb">
                  {category.image_url
                    ? <img src={category.image_url} alt=""/>
                    : <span className="adm-cat-modal-thumb-ph">no image</span>}
                </div>
                <input
                  ref={fileRef}
                  type="file"
                  accept="image/png,image/jpeg,image/webp,image/gif"
                  style={{ display: 'none' }}
                  onChange={(e) => uploadImage(e.target.files && e.target.files[0])}
                />
                <button
                  className="btn ghost"
                  onClick={() => fileRef.current && fileRef.current.click()}
                  disabled={busy === 'upload'}
                ><span>{busy === 'upload' ? 'UPLOADING…' : '↑ UPLOAD BANNER'}</span></button>
              </div>
            </label>

            {savedAt && <div className="adm-cat-modal-ok">✓ saved</div>}
            {error && <div className="adm-cat-create-err">! {error}</div>}

            <div className="adm-cat-modal-foot">
              <button
                className="btn ghost adm-cat-delete"
                onClick={doDelete}
                disabled={busy === 'delete'}
              ><span>{busy === 'delete' ? 'DELETING…' : '✕ DELETE CATEGORY'}</span></button>
            </div>
          </div>
        </div>
      </div>
    );
  }

  // ---------- DRAFT EDITOR (used for "+ NEW PRODUCT") ----------
  //
  // Mirrors ProductEditor's chrome (same BASIC + PRICING + FEATURES + IMAGES
  // cards) so creating feels identical to editing. The difference: nothing
  // is sent to D1 until the user clicks CREATE PRODUCT at the bottom. That
  // single submit runs three calls in order:
  //   1. POST  /api/admin/products            → returns the new id
  //   2. PUT   /api/admin/products/:id/features (if any features entered)
  //   3. POST  /api/admin/products/:id/images for each staged file
  // Then ExecCatalog refreshes and the user lands in the live ProductEditor
  // for the freshly-created product.
  function ProductDraftEditor({ onBack, onCreated }) {
    const [category, setCategory] = useState(null);
    const [name, setName] = useState('');
    const [idEdited, setIdEdited] = useState(false);
    const [idValue, setIdValue] = useState('');
    const [version, setVersion] = useState('v1.0.0');
    const [blurb, setBlurb] = useState('');
    const [description, setDescription] = useState('');
    const [runtime, setRuntime] = useState('Windows 10 / 11');
    const [size, setSize] = useState('External loader');
    const [license, setLicense] = useState('Time-based');
    const [featured, setFeatured] = useState(false);
    const [day,   setDay]   = useState('');
    const [week,  setWeek]  = useState('');
    const [month, setMonth] = useState('');
    const [stateChoice, setStateChoice] = useState('updating'); // start unverified
    const [stateNote,   setStateNote]   = useState('');
    const [featuresRaw, setFeaturesRaw] = useState('');
    const [stagedImages, setStagedImages] = useState([]);   // [{file, previewUrl}]
    const [busy, setBusy] = useState(false);
    const [error, setError] = useState(null);

    // Derive id from name unless the user has explicitly edited it.
    useEffect(() => {
      if (idEdited) return;
      setIdValue(slugify(name));
    }, [name, idEdited]);

    // Clean up object URLs we created for staged-image previews.
    useEffect(() => () => stagedImages.forEach((s) => URL.revokeObjectURL(s.previewUrl)), []);

    const parsedFeatures = useMemo(() => parseFeatures(featuresRaw), [featuresRaw]);

    const onFilePicked = (files) => {
      const next = Array.from(files || []).map((f) => ({
        file: f,
        previewUrl: URL.createObjectURL(f),
      }));
      setStagedImages((prev) => prev.concat(next));
    };
    const removeStaged = (i) => {
      setStagedImages((prev) => {
        const tile = prev[i];
        if (tile) URL.revokeObjectURL(tile.previewUrl);
        return prev.filter((_, j) => j !== i);
      });
    };

    const canSubmit = !busy && !!category && !!name.trim() && !!idValue.trim();

    const onSubmit = async () => {
      if (!canSubmit) return;
      setBusy(true); setError(null);
      try {
        // 1. Create the product row with every text/number field the user
        //    filled in. The Worker silently ignores anything not in its
        //    whitelist, so adding extra keys here is safe.
        const createRes = await fetch('/api/admin/products', {
          method: 'POST',
          credentials: 'same-origin',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            id: idValue.trim(),
            name: name.trim(),
            category,
            version: version.trim(),
            blurb: blurb.trim(),
            description: description.trim(),
            runtime: runtime.trim(),
            size: size.trim(),
            license: license.trim(),
            featured,
            price_day_cents:   Math.round((Number(day)   || 0) * 100),
            price_week_cents:  Math.round((Number(week)  || 0) * 100),
            price_month_cents: Math.round((Number(month) || 0) * 100),
          }),
        });
        const createData = await createRes.json();
        if (!createRes.ok) throw new Error(createData.error || ('http_' + createRes.status));
        const newId = createData.id;

        // 2. Push the chosen operational state. Worker treats this as an
        //    upsert so writing 'updating' (the default) is harmless.
        if (window.ExecStates) {
          try { await window.ExecStates.setState(newId, stateChoice, stateNote.trim()); }
          catch { /* state write is non-fatal — admin can fix it from the row */ }
        }

        // 3. Replace features list in one shot if any were entered.
        if (parsedFeatures.length) {
          await fetch(`/api/admin/products/${encodeURIComponent(newId)}/features`, {
            method: 'PUT',
            credentials: 'same-origin',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ features: parsedFeatures }),
          });
        }

        // 4. Upload staged images sequentially so order matches the staging UI.
        for (const tile of stagedImages) {
          const f = tile.file;
          if (!f || !f.type.startsWith('image/')) continue;
          await fetch(`/api/admin/products/${encodeURIComponent(newId)}/images`, {
            method: 'POST',
            credentials: 'same-origin',
            headers: { 'Content-Type': f.type },
            body: f,
          });
        }

        onCreated && onCreated(newId);
      } catch (e) {
        setError(String(e.message || e));
        setBusy(false);
      }
    };

    return (
      <>
        <div className="adm-cat-back" onClick={busy ? null : onBack}>
          <span>← BACK TO CATALOG</span>
        </div>

        <div className="ap-sec-head">
          <div className="ap-sec-tag">// NEW</div>
          <h2 className="ap-sec-title">
            {name || 'NEW PRODUCT'}
            {idValue && <span className="ap-sec-count">/ {idValue}</span>}
          </h2>
        </div>

        <div className="adm-cat-grid">
          {/* BASIC card ---------------------------------------------------- */}
          <section className="adm-cat-card">
            <window.CornerCuts size={8}/>
            <div className="adm-cat-card-h">BASIC</div>
            <div className="adm-cat-card-body">

              <label className="adm-cat-field">
                <span className="adm-cat-field-l">NAME <span className="adm-cat-req">· required</span></span>
                <input
                  type="text"
                  className="adm-cat-input"
                  value={name}
                  onChange={(e) => setName(e.target.value)}
                  placeholder="e.g. FECURITY EFT"
                  autoFocus
                />
              </label>

              <label className="adm-cat-field">
                <span className="adm-cat-field-l">
                  ID
                  <span className="adm-cat-req"> · {idEdited ? 'custom' : 'auto from name'}</span>
                  {idEdited && (
                    <button
                      type="button"
                      className="adm-new-id-reset"
                      onClick={() => { setIdEdited(false); setIdValue(slugify(name)); }}
                    >reset</button>
                  )}
                </span>
                <input
                  type="text"
                  className="adm-cat-input"
                  value={idValue}
                  onChange={(e) => { setIdEdited(true); setIdValue(slugify(e.target.value)); }}
                  placeholder="auto-derived from name"
                />
              </label>

              <label className="adm-cat-field">
                <span className="adm-cat-field-l">CATEGORY <span className="adm-cat-req">· required</span></span>
                <CategoryPicker value={category} onChange={setCategory}/>
              </label>

              <label className="adm-cat-field">
                <span className="adm-cat-field-l">VERSION</span>
                <input
                  type="text"
                  className="adm-cat-input"
                  value={version}
                  onChange={(e) => setVersion(e.target.value)}
                  placeholder="v1.0.0"
                />
              </label>

              <label className="adm-cat-field">
                <span className="adm-cat-field-l">BLURB</span>
                <input
                  type="text"
                  className="adm-cat-input"
                  value={blurb}
                  onChange={(e) => setBlurb(e.target.value)}
                  placeholder="short tagline"
                  maxLength={500}
                />
              </label>

              <label className="adm-cat-field">
                <span className="adm-cat-field-l">DESCRIPTION</span>
                <textarea
                  className="adm-cat-input"
                  rows={5}
                  value={description}
                  onChange={(e) => setDescription(e.target.value)}
                  placeholder="full product description shown in the modal"
                />
              </label>

              <label className="adm-cat-field">
                <span className="adm-cat-field-l">RUNTIME</span>
                <input
                  type="text"
                  className="adm-cat-input"
                  value={runtime}
                  onChange={(e) => setRuntime(e.target.value)}
                  placeholder="Windows 10 / 11"
                />
              </label>

              <label className="adm-cat-field">
                <span className="adm-cat-field-l">SIZE</span>
                <input
                  type="text"
                  className="adm-cat-input"
                  value={size}
                  onChange={(e) => setSize(e.target.value)}
                  placeholder="External loader"
                />
              </label>

              <label className="adm-cat-field">
                <span className="adm-cat-field-l">LICENSE</span>
                <input
                  type="text"
                  className="adm-cat-input"
                  value={license}
                  onChange={(e) => setLicense(e.target.value)}
                  placeholder="Time-based"
                />
              </label>
            </div>
          </section>

          {/* PRICING card -------------------------------------------------- */}
          <section className="adm-cat-card">
            <window.CornerCuts size={8}/>
            <div className="adm-cat-card-h">PRICING <span className="adm-cat-h-sub">enter dollars · stored in cents</span></div>
            <div className="adm-cat-card-body">
              <label className="adm-cat-field">
                <span className="adm-cat-field-l">DAY ($)</span>
                <input type="number" min="0" step="0.01" className="adm-cat-input"
                  value={day}   onChange={(e) => setDay(e.target.value)}   placeholder="0"/>
              </label>
              <label className="adm-cat-field">
                <span className="adm-cat-field-l">WEEK ($)</span>
                <input type="number" min="0" step="0.01" className="adm-cat-input"
                  value={week}  onChange={(e) => setWeek(e.target.value)}  placeholder="0"/>
              </label>
              <label className="adm-cat-field">
                <span className="adm-cat-field-l">MONTH ($)</span>
                <input type="number" min="0" step="0.01" className="adm-cat-input"
                  value={month} onChange={(e) => setMonth(e.target.value)} placeholder="0"/>
              </label>
              <label className="adm-cat-field adm-cat-toggle-field">
                <span className="adm-cat-field-l">FEATURED</span>
                <label className="adm-cat-toggle">
                  <input type="checkbox" checked={featured} onChange={(e) => setFeatured(e.target.checked)}/>
                  <span>{featured ? 'ON' : 'OFF'}</span>
                </label>
              </label>
            </div>
          </section>

          {/* STATUS card --------------------------------------------------- */}
          <section className="adm-cat-card adm-cat-card--wide">
            <window.CornerCuts size={8}/>
            <div className="adm-cat-card-h">
              STATUS <span className="adm-cat-h-sub">visible to every visitor immediately after create</span>
            </div>
            <div className="adm-cat-card-body adm-cat-card-body--row">
              <label className="adm-cat-field">
                <span className="adm-cat-field-l">OPERATIONAL STATE</span>
                <div className={window.cls('adm-cat-state', 'adm-cat-state--' + stateChoice, 'adm-cat-state--lg')}>
                  <span className="adm-cat-state-dot"/>
                  <select
                    className="adm-cat-state-select"
                    value={stateChoice}
                    onChange={(e) => setStateChoice(e.target.value)}
                  >
                    <option value="online">ONLINE</option>
                    <option value="updating">UPDATING</option>
                    <option value="offline">OFFLINE</option>
                  </select>
                </div>
              </label>
              <label className="adm-cat-field" style={{ flex: 1 }}>
                <span className="adm-cat-field-l">NOTE (optional)</span>
                <input
                  type="text"
                  className="adm-cat-input"
                  value={stateNote}
                  onChange={(e) => setStateNote(e.target.value)}
                  placeholder="e.g. v0.9.7 patch deploying"
                  maxLength={240}
                />
              </label>
            </div>
          </section>

          {/* FEATURES card ------------------------------------------------- */}
          <section className="adm-cat-card adm-cat-card--wide">
            <window.CornerCuts size={8}/>
            <div className="adm-cat-card-h">
              FEATURES <span className="adm-cat-h-sub">paste comma / newline / [bracket] / (paren) separated</span>
            </div>
            <div className="adm-cat-card-body">
              <textarea
                className="adm-cat-input"
                rows={6}
                value={featuresRaw}
                onChange={(e) => setFeaturesRaw(e.target.value)}
                placeholder={
                  'Aimbot, Player ESP, Loot ESP\n' +
                  '— or —\n' +
                  '[Aimbot] [Player ESP] [Loot ESP]\n' +
                  '— or —\n' +
                  'one feature per line'
                }
              />
              {parsedFeatures.length > 0 && (
                <div className="adm-new-feat-preview">
                  <div className="adm-new-feat-h">PREVIEW · {parsedFeatures.length} feature{parsedFeatures.length === 1 ? '' : 's'}</div>
                  <ul>
                    {parsedFeatures.map((f, i) => (
                      <li key={i}><span className="adm-new-feat-idx">{String(i+1).padStart(2,'0')}</span>{f}</li>
                    ))}
                  </ul>
                </div>
              )}
            </div>
          </section>

          {/* IMAGES card --------------------------------------------------- */}
          <section className="adm-cat-card adm-cat-card--wide">
            <window.CornerCuts size={8}/>
            <div className="adm-cat-card-h">
              IMAGES <span className="adm-cat-h-sub">optional · first one is the cover</span>
            </div>
            <div className="adm-cat-card-body">
              <ImageStager files={stagedImages} onAdd={onFilePicked} onRemove={removeStaged}/>
            </div>
          </section>
        </div>

        {error && <div className="adm-new-error">⚠ {error}</div>}

        <div className="adm-draft-actions">
          <button className="btn ghost"   onClick={onBack}   disabled={busy}><span>CANCEL</span></button>
          <button className="btn primary" onClick={onSubmit} disabled={!canSubmit}>
            <span>{busy ? 'CREATING…' : '＋ CREATE PRODUCT'}</span>
          </button>
        </div>
      </>
    );
  }

  function ImageStager({ files, onAdd, onRemove }) {
    const ref = useRef(null);
    const [dragOver, setDragOver] = useState(false);
    const onDrop = (e) => {
      e.preventDefault();
      setDragOver(false);
      onAdd(e.dataTransfer.files);
    };
    return (
      <div>
        <div
          className={window.cls('adm-new-drop', dragOver && 'over')}
          onClick={() => ref.current && ref.current.click()}
          onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
          onDragLeave={() => setDragOver(false)}
          onDrop={onDrop}
        >
          <span>＋ DROP IMAGES HERE OR CLICK TO UPLOAD</span>
          <input
            ref={ref}
            type="file"
            multiple
            accept="image/png,image/jpeg,image/webp,image/gif"
            style={{ display: 'none' }}
            onChange={(e) => { onAdd(e.target.files); e.target.value = ''; }}
          />
        </div>
        {files.length > 0 && (
          <div className="adm-new-img-grid">
            {files.map((tile, i) => (
              <div key={tile.previewUrl} className={window.cls('adm-new-img-tile', i === 0 && 'cover')}>
                <img src={tile.previewUrl} alt=""/>
                <button className="adm-new-img-x" onClick={() => onRemove(i)} type="button" aria-label="Remove">✕</button>
                {i === 0 && <span className="adm-new-img-flag">COVER</span>}
              </div>
            ))}
          </div>
        )}
      </div>
    );
  }

  // ---------- helpers ----------
  function slugify(s) {
    return String(s || '')
      .toLowerCase()
      .replace(/[^a-z0-9-]+/g, '-')
      .replace(/^-+|-+$/g, '')
      .slice(0, 60);
  }

  // Accepts free-form text and returns an ordered, de-duped list of features.
  // Priority of delimiters: newlines → bracketed/paren'd groups → semicolons →
  // commas. Falls back to a single-item list if none of those split the input.
  function parseFeatures(raw) {
    const s = String(raw || '').trim();
    if (!s) return [];

    if (s.includes('\n')) return clean(s.split(/\r?\n/));

    const bracketed = [...s.matchAll(/[\[(]([^\])]+)[\])]/g)].map((m) => m[1]);
    if (bracketed.length >= 2) return clean(bracketed);

    if (s.includes(';'))  return clean(s.split(';'));
    if (s.includes(','))  return clean(s.split(','));
    return clean([s]);
  }
  function clean(arr) {
    const seen = new Set();
    const out = [];
    for (const piece of arr) {
      const t = String(piece).trim().replace(/^[•\-\*\d.]+\s*/, '');
      if (!t) continue;
      const key = t.toLowerCase();
      if (seen.has(key)) continue;
      seen.add(key);
      out.push(t);
    }
    return out;
  }

  // ---------- EDITOR VIEW ----------
  function ProductEditor({ product, onBack, onRefresh }) {
    return (
      <>
        <div className="adm-cat-back" onClick={onBack}>
          <span>← BACK TO CATALOG</span>
        </div>

        <div className="ap-sec-head">
          <div className="ap-sec-tag">// EDIT</div>
          <h2 className="ap-sec-title">{product.name} <span className="ap-sec-count">/ {product.tag}</span></h2>
          <div className="ap-sec-side">
            <DeleteButton product={product} onDone={onBack}/>
          </div>
        </div>

        <div className="adm-cat-grid">
          <BasicFields product={product} onSaved={onRefresh}/>
          <PricingFields product={product} onSaved={onRefresh}/>
          <StatusCard product={product}/>
          <LoaderCard product={product} onSaved={onRefresh}/>
          <FeaturesEditor product={product} onSaved={onRefresh}/>
          <ImagesEditor product={product} onSaved={onRefresh}/>
        </div>
      </>
    );
  }

  function DeleteButton({ product, onDone }) {
    const [busy, setBusy] = useState(false);
    const onClick = async () => {
      if (!window.confirm(
        `Delete "${product.name}"?\n\nThis removes the product from D1 and all its images from R2. Cannot be undone.`
      )) return;
      setBusy(true);
      try {
        const res = await fetch(`/api/admin/products/${encodeURIComponent(product.id)}`, {
          method: 'DELETE', credentials: 'same-origin',
        });
        if (!res.ok) { alert('Delete failed: ' + res.status); setBusy(false); return; }
        await (window.ExecCatalog && window.ExecCatalog.fetchAll({ fresh: true }));
        onDone();
      } catch (e) { alert('Delete failed: ' + e.message); setBusy(false); }
    };
    return (
      <button className="btn ghost adm-cat-delete" onClick={onClick} disabled={busy}>
        <span>{busy ? 'DELETING…' : '✕ DELETE PRODUCT'}</span>
      </button>
    );
  }

  // ---------- field group: name/tag/category/version/etc ----------
  function BasicFields({ product, onSaved }) {
    return (
      <section className="adm-cat-card">
        <window.CornerCuts size={8}/>
        <div className="adm-cat-card-h">BASIC</div>
        <div className="adm-cat-card-body">
          <Field product={product} field="name"        label="NAME"        onSaved={onSaved}/>
          <Field product={product} field="tag"         label="TAG"         onSaved={onSaved}/>
          <CategoryField product={product} onSaved={onSaved}/>
          <ToggleField product={product} field="featured" label="FEATURED" onSaved={onSaved}/>
          <Field product={product} field="version" label="VERSION" onSaved={onSaved}/>
          <Field product={product} field="blurb"   label="BLURB"   onSaved={onSaved} placeholder="short tagline"/>
          <Field product={product} field="description" label="DESCRIPTION" onSaved={onSaved} multiline/>
          <Field product={product} field="runtime" label="RUNTIME" onSaved={onSaved}/>
          <Field product={product} field="size"    label="SIZE"    onSaved={onSaved}/>
          <Field product={product} field="license" label="LICENSE" onSaved={onSaved}/>
        </div>
      </section>
    );
  }

  function PricingFields({ product, onSaved }) {
    return (
      <section className="adm-cat-card">
        <window.CornerCuts size={8}/>
        <div className="adm-cat-card-h">PRICING <span className="adm-cat-h-sub">stored in cents · enter dollars</span></div>
        <div className="adm-cat-card-body">
          <PriceField product={product} field="price_day_cents"   label="DAY ($)"   currentDollars={product.pricing?.day   || 0} onSaved={onSaved}/>
          <PriceField product={product} field="price_week_cents"  label="WEEK ($)"  currentDollars={product.pricing?.week  || 0} onSaved={onSaved}/>
          <PriceField product={product} field="price_month_cents" label="MONTH ($)" currentDollars={product.pricing?.month || 0} onSaved={onSaved}/>
          <SortField product={product} onSaved={onSaved}/>
        </div>
      </section>
    );
  }

  // ---------- live editor STATUS card ----------
  function StatusCard({ product }) {
    window.ExecStates && window.ExecStates.useStates();
    const cur = window.ExecStates ? window.ExecStates.get(product.id) : { state: 'updating', note: '' };

    const [note, setNote] = useState(cur.note || '');
    const [saving, setSaving] = useState(null);     // null | 'state' | 'note'
    const [savedAt, setSavedAt] = useState(null);
    useEffect(() => { setNote(cur.note || ''); }, [cur.note]);

    const save = async (nextState, nextNote, kind) => {
      setSaving(kind);
      try {
        await window.ExecStates.setState(product.id, nextState, nextNote);
        setSavedAt(Date.now());
      } finally { setSaving(null); }
    };
    const onPickState = (e) => save(e.target.value, note, 'state');
    const onBlurNote  = ()  => { if ((note || '') !== (cur.note || '')) save(cur.state, note, 'note'); };

    return (
      <section className="adm-cat-card">
        <window.CornerCuts size={8}/>
        <div className="adm-cat-card-h">
          STATUS <span className="adm-cat-h-sub">visible to every visitor immediately</span>
        </div>
        <div className="adm-cat-card-body">
          <label className="adm-cat-field">
            <span className="adm-cat-field-l">
              OPERATIONAL STATE
              {saving === 'state' && <span className="adm-state-saving"> · saving</span>}
              {!saving && savedAt && <span className="adm-state-ok"> · saved</span>}
            </span>
            <div className={window.cls('adm-cat-state', 'adm-cat-state--' + cur.state, 'adm-cat-state--lg')}>
              <span className="adm-cat-state-dot"/>
              <select className="adm-cat-state-select" value={cur.state} onChange={onPickState} disabled={saving === 'state'}>
                <option value="online">ONLINE</option>
                <option value="updating">UPDATING</option>
                <option value="offline">OFFLINE</option>
              </select>
            </div>
          </label>
          <label className="adm-cat-field">
            <span className="adm-cat-field-l">
              NOTE
              {saving === 'note' && <span className="adm-state-saving"> · saving</span>}
            </span>
            <input
              type="text"
              className="adm-cat-input"
              value={note}
              onChange={(e) => setNote(e.target.value)}
              onBlur={onBlurNote}
              onKeyDown={(e) => { if (e.key === 'Enter') e.currentTarget.blur(); }}
              placeholder="e.g. v0.9.7 patch deploying"
              maxLength={240}
              disabled={saving === 'note'}
            />
          </label>
        </div>
      </section>
    );
  }

  // ---------- LOADER card ----------
  // One external URL per product. Customers who have a paid order for this
  // product will see a DOWNLOAD LOADER button that 302-redirects to whatever
  // URL is set here (mega, github releases, our own CDN — anything https).
  // Empty value clears it; the customer's button then disappears entirely.
  function LoaderCard({ product, onSaved }) {
    const initial = product.loader_url || '';
    const [val, setVal] = useState(initial);
    const [state, setState] = useState(null);   // null | 'saving' | 'ok' | 'err'
    const [err, setErr] = useState('');
    useEffect(() => { setVal(product.loader_url || ''); }, [product.loader_url]);

    const save = async () => {
      const trimmed = val.trim();
      if (trimmed === initial) { setErr(''); return; }
      if (trimmed.length > 500) {
        setErr('url too long (max 500 chars)'); setState('err'); return;
      }
      setErr(''); setState('saving');
      try {
        await patchProduct(product.id, { loader_url: trimmed });
        setState('ok');
        // The worker may have auto-prepended https://, so pull fresh data
        // before our local `val` falls out of sync with what's stored.
        await (window.ExecCatalog && window.ExecCatalog.fetchAll({ fresh: true }));
        onSaved && onSaved();
      } catch { setState('err'); setErr('save failed'); }
    };

    const clear = async () => {
      if (!initial) { setVal(''); return; }
      if (!window.confirm('Remove the loader URL?\n\nCustomers who already own this product will no longer see a download button.')) return;
      setVal('');
      setErr(''); setState('saving');
      try {
        await patchProduct(product.id, { loader_url: '' });
        setState('ok');
        await (window.ExecCatalog && window.ExecCatalog.fetchAll({ fresh: true }));
        onSaved && onSaved();
      } catch { setState('err'); setErr('save failed'); }
    };

    return (
      <section className="adm-cat-card adm-cat-card--wide">
        <window.CornerCuts size={8}/>
        <div className="adm-cat-card-h">
          LOADER <span className="adm-cat-h-sub">external download URL · customers with a paid order get a button that redirects here</span>
        </div>
        <div className="adm-cat-card-body">
          <label className="adm-cat-field">
            <span className="adm-cat-field-l">
              LOADER URL
              {err
                ? <span className="adm-state-err"> · {err}</span>
                : <FieldState state={state}/>}
            </span>
            <input
              type="url"
              className="adm-cat-input"
              value={val}
              onChange={(e) => setVal(e.target.value)}
              onBlur={save}
              onKeyDown={(e) => { if (e.key === 'Enter') e.currentTarget.blur(); }}
              placeholder="mega.nz/file/... · github.com/org/repo/releases/.../loader.exe · anywhere"
              maxLength={500}
            />
          </label>
          {initial && (
            <div className="adm-cat-loader-row">
              <a href={initial} target="_blank" rel="noopener noreferrer" className="adm-cat-loader-test">
                ↗ TEST LINK
              </a>
              <button type="button" className="adm-mini-btn" onClick={clear}>CLEAR</button>
            </div>
          )}
        </div>
      </section>
    );
  }

  // ---------- generic patch helpers ----------
  async function patchProduct(productId, body) {
    const res = await fetch(`/api/admin/products/${encodeURIComponent(productId)}`, {
      method: 'PATCH',
      credentials: 'same-origin',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(body),
    });
    if (!res.ok) {
      const t = await res.text().catch(() => '');
      throw new Error('save_failed: ' + res.status + ' ' + t);
    }
    return res.json();
  }

  function Field({ product, field, label, onSaved, multiline, placeholder }) {
    const initial = product[field] != null ? product[field] : '';
    const [val, setVal] = useState(initial);
    const [state, setState] = useState(null);   // null | 'saving' | 'ok' | 'err'
    useEffect(() => { setVal(product[field] != null ? product[field] : ''); }, [product[field]]);

    const save = async () => {
      if (val === initial) return;
      setState('saving');
      try {
        await patchProduct(product.id, { [field]: val });
        setState('ok');
        await (window.ExecCatalog && window.ExecCatalog.fetchAll({ fresh: true }));
        onSaved && onSaved();
      } catch { setState('err'); }
    };

    return (
      <label className="adm-cat-field">
        <span className="adm-cat-field-l">{label} <FieldState state={state}/></span>
        {multiline
          ? <textarea
              value={val}
              onChange={(e) => setVal(e.target.value)}
              onBlur={save}
              placeholder={placeholder || ''}
              rows={5}
              className="adm-cat-input"
            />
          : <input
              type="text"
              value={val}
              onChange={(e) => setVal(e.target.value)}
              onBlur={save}
              onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) e.currentTarget.blur(); }}
              placeholder={placeholder || ''}
              className="adm-cat-input"
            />
        }
      </label>
    );
  }

  // Wraps CategoryPicker for the live ProductEditor: changing the chip
  // selection PATCHes the product immediately and refreshes the catalog.
  // Also surfaces the inline "+ NEW" + "✎ manage" affordances here so
  // admins can create categories without leaving the product editor.
  function CategoryField({ product, onSaved }) {
    const [state, setState] = useState(null);
    const onChange = async (next) => {
      if (!next || next === product.category) return;
      setState('saving');
      try {
        await patchProduct(product.id, { category: next });
        setState('ok');
        await (window.ExecCatalog && window.ExecCatalog.fetchAll({ fresh: true }));
        onSaved && onSaved();
      } catch { setState('err'); }
    };
    return (
      <label className="adm-cat-field">
        <span className="adm-cat-field-l">CATEGORY <FieldState state={state}/></span>
        <CategoryPicker value={product.category} onChange={onChange}/>
      </label>
    );
  }

  function SelectField({ product, field, label, options, onSaved }) {
    const [state, setState] = useState(null);
    const onChange = async (e) => {
      const next = e.target.value;
      if (next === product[field]) return;
      setState('saving');
      try {
        await patchProduct(product.id, { [field]: next });
        setState('ok');
        await (window.ExecCatalog && window.ExecCatalog.fetchAll({ fresh: true }));
        onSaved && onSaved();
      } catch { setState('err'); }
    };
    return (
      <label className="adm-cat-field">
        <span className="adm-cat-field-l">{label} <FieldState state={state}/></span>
        <select className="adm-cat-input adm-cat-select" value={product[field] || ''} onChange={onChange}>
          {options.map((o) => <option key={o.id} value={o.id}>{o.label}</option>)}
        </select>
      </label>
    );
  }

  function ToggleField({ product, field, label, onSaved }) {
    const [state, setState] = useState(null);
    const onChange = async (e) => {
      setState('saving');
      try {
        await patchProduct(product.id, { [field]: e.target.checked });
        setState('ok');
        await (window.ExecCatalog && window.ExecCatalog.fetchAll({ fresh: true }));
        onSaved && onSaved();
      } catch { setState('err'); }
    };
    return (
      <label className="adm-cat-field adm-cat-toggle-field">
        <span className="adm-cat-field-l">{label} <FieldState state={state}/></span>
        <label className="adm-cat-toggle">
          <input type="checkbox" checked={!!product[field]} onChange={onChange}/>
          <span>{product[field] ? 'ON' : 'OFF'}</span>
        </label>
      </label>
    );
  }

  function PriceField({ product, field, label, currentDollars, onSaved }) {
    const [val, setVal] = useState(String(currentDollars));
    const [state, setState] = useState(null);
    useEffect(() => { setVal(String(currentDollars)); }, [currentDollars]);

    const save = async () => {
      const dollars = Number(val);
      if (!Number.isFinite(dollars) || dollars < 0) { setVal(String(currentDollars)); return; }
      const cents = Math.round(dollars * 100);
      const currentCents = Math.round(currentDollars * 100);
      if (cents === currentCents) return;
      setState('saving');
      try {
        await patchProduct(product.id, { [field]: cents });
        setState('ok');
        await (window.ExecCatalog && window.ExecCatalog.fetchAll({ fresh: true }));
        onSaved && onSaved();
      } catch { setState('err'); }
    };

    return (
      <label className="adm-cat-field">
        <span className="adm-cat-field-l">{label} <FieldState state={state}/></span>
        <input
          type="number" min="0" step="0.01"
          className="adm-cat-input"
          value={val}
          onChange={(e) => setVal(e.target.value)}
          onBlur={save}
          onKeyDown={(e) => { if (e.key === 'Enter') e.currentTarget.blur(); }}
        />
      </label>
    );
  }

  function SortField({ product, onSaved }) {
    const [val, setVal] = useState(String(product.sort_order || 0));
    const [state, setState] = useState(null);
    useEffect(() => { setVal(String(product.sort_order || 0)); }, [product.sort_order]);

    const save = async () => {
      const n = parseInt(val, 10);
      if (!Number.isFinite(n)) { setVal(String(product.sort_order || 0)); return; }
      if (n === (product.sort_order || 0)) return;
      setState('saving');
      try {
        await patchProduct(product.id, { sort_order: n });
        setState('ok');
        await (window.ExecCatalog && window.ExecCatalog.fetchAll({ fresh: true }));
        onSaved && onSaved();
      } catch { setState('err'); }
    };

    return (
      <label className="adm-cat-field">
        <span className="adm-cat-field-l">SORT ORDER <FieldState state={state}/></span>
        <input
          type="number" step="1"
          className="adm-cat-input"
          value={val}
          onChange={(e) => setVal(e.target.value)}
          onBlur={save}
          onKeyDown={(e) => { if (e.key === 'Enter') e.currentTarget.blur(); }}
        />
      </label>
    );
  }

  function FieldState({ state }) {
    if (!state) return null;
    if (state === 'saving') return <span className="adm-state-saving">· saving</span>;
    if (state === 'ok')     return <span className="adm-state-ok">· saved</span>;
    if (state === 'err')    return <span className="adm-state-err">· save failed</span>;
    return null;
  }

  // ---------- FEATURES EDITOR ----------
  function FeaturesEditor({ product, onSaved }) {
    const [features, setFeatures] = useState((product.features || []).slice());
    const [busy, setBusy] = useState(false);
    const [dirty, setDirty] = useState(false);
    // Only resync from the server prop when the admin opens a different
    // product. Watching `product.features` directly would clobber in-progress
    // edits because ExecCatalog deep-clones on every refresh (so the array
    // reference changes after every save, even though the contents are what
    // we just wrote).
    useEffect(() => {
      setFeatures((product.features || []).slice());
      setDirty(false);
    }, [product.id]);

    const save = async (next) => {
      setBusy(true);
      try {
        await fetch(`/api/admin/products/${encodeURIComponent(product.id)}/features`, {
          method: 'PUT', credentials: 'same-origin',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ features: next }),
        });
        await (window.ExecCatalog && window.ExecCatalog.fetchAll({ fresh: true }));
        setDirty(false);
        onSaved && onSaved();
      } finally { setBusy(false); }
    };

    const setOne = (i, v)   => { const next = features.slice(); next[i] = v; setFeatures(next); setDirty(true); };
    const removeOne = (i)   => { const next = features.slice(); next.splice(i, 1); setFeatures(next); save(next); };
    const addOne = ()       => { const next = features.concat(['']); setFeatures(next); setDirty(true); };
    const move = (i, delta) => {
      const j = i + delta;
      if (j < 0 || j >= features.length) return;
      const next = features.slice();
      [next[i], next[j]] = [next[j], next[i]];
      setFeatures(next); save(next);
    };

    return (
      <section className="adm-cat-card adm-cat-card--wide">
        <window.CornerCuts size={8}/>
        <div className="adm-cat-card-h">
          FEATURES <span className="adm-cat-h-sub">{features.length} item{features.length === 1 ? '' : 's'}</span>
          <span style={{ marginLeft: 'auto' }}>
            {busy && <span className="adm-state-saving">saving…</span>}
            {dirty && !busy && (
              <button className="btn ghost adm-cat-h-btn" onClick={() => save(features)}>SAVE ORDER + EDITS</button>
            )}
          </span>
        </div>
        <div className="adm-cat-card-body">
          <ul className="adm-feat-list">
            {features.map((f, i) => (
              <li key={i} className="adm-feat-row">
                <span className="adm-feat-idx">{String(i + 1).padStart(2, '0')}</span>
                <input
                  type="text"
                  className="adm-cat-input"
                  value={f}
                  onChange={(e) => setOne(i, e.target.value)}
                  onBlur={() => dirty && save(features)}
                  onKeyDown={(e) => { if (e.key === 'Enter') e.currentTarget.blur(); }}
                />
                <button className="adm-feat-btn" title="Move up"   onClick={() => move(i, -1)} disabled={i === 0}>↑</button>
                <button className="adm-feat-btn" title="Move down" onClick={() => move(i, +1)} disabled={i === features.length - 1}>↓</button>
                <button className="adm-feat-btn danger" title="Remove" onClick={() => removeOne(i)}>✕</button>
              </li>
            ))}
          </ul>
          <button className="btn ghost adm-feat-add" onClick={addOne}><span>＋ ADD FEATURE</span></button>
        </div>
      </section>
    );
  }

  // ---------- IMAGES EDITOR ----------
  function ImagesEditor({ product, onSaved }) {
    const [busy, setBusy] = useState(false);
    const [error, setError] = useState(null);
    const fileRef = useRef(null);
    const images = product.images || [];

    const upload = async (file) => {
      if (!file) return;
      setBusy(true); setError(null);
      try {
        const res = await fetch(`/api/admin/products/${encodeURIComponent(product.id)}/images`, {
          method: 'POST',
          credentials: 'same-origin',
          headers: { 'Content-Type': file.type },
          body: file,
        });
        if (!res.ok) {
          const t = await res.text().catch(() => '');
          throw new Error(res.status + ' ' + t);
        }
        await (window.ExecCatalog && window.ExecCatalog.fetchAll({ fresh: true }));
        onSaved && onSaved();
      } catch (e) { setError(String(e.message || e)); }
      finally { setBusy(false); }
    };

    const removeAt = async (idx) => {
      if (!window.confirm('Remove this image?')) return;
      setBusy(true); setError(null);
      try {
        const res = await fetch(`/api/admin/products/${encodeURIComponent(product.id)}/images/${idx}`, {
          method: 'DELETE', credentials: 'same-origin',
        });
        if (!res.ok) throw new Error(res.status);
        await (window.ExecCatalog && window.ExecCatalog.fetchAll({ fresh: true }));
        onSaved && onSaved();
      } catch (e) { setError(String(e.message || e)); }
      finally { setBusy(false); }
    };

    const move = async (i, delta) => {
      const j = i + delta;
      if (j < 0 || j >= images.length) return;
      const next = images.slice();
      [next[i], next[j]] = [next[j], next[i]];
      setBusy(true);
      try {
        await fetch(`/api/admin/products/${encodeURIComponent(product.id)}/images`, {
          method: 'PUT', credentials: 'same-origin',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ urls: next }),
        });
        await (window.ExecCatalog && window.ExecCatalog.fetchAll({ fresh: true }));
        onSaved && onSaved();
      } finally { setBusy(false); }
    };

    return (
      <section className="adm-cat-card adm-cat-card--wide">
        <window.CornerCuts size={8}/>
        <div className="adm-cat-card-h">
          IMAGES <span className="adm-cat-h-sub">{images.length} · idx 0 = cover</span>
          <span style={{ marginLeft: 'auto' }}>
            {busy && <span className="adm-state-saving">working…</span>}
            {error && <span className="adm-state-err" title={error}>· failed</span>}
            <button className="btn ghost adm-cat-h-btn" onClick={() => fileRef.current && fileRef.current.click()}>
              ＋ UPLOAD
            </button>
            <input
              type="file"
              ref={fileRef}
              style={{ display: 'none' }}
              accept="image/png,image/jpeg,image/webp,image/gif"
              onChange={(e) => { const f = e.target.files && e.target.files[0]; e.target.value = ''; upload(f); }}
            />
          </span>
        </div>
        <div className="adm-cat-card-body">
          <div className="adm-img-grid">
            {images.length === 0 && (
              <div className="adm-img-empty">no images yet · click UPLOAD to add the first one</div>
            )}
            {images.map((url, i) => (
              <div key={url + i} className={window.cls('adm-img-tile', i === 0 && 'cover')}>
                <img src={url} alt=""/>
                <div className="adm-img-tile-bar">
                  <span className="adm-img-idx">{i === 0 ? 'COVER' : '#' + String(i + 1).padStart(2, '0')}</span>
                  <button className="adm-feat-btn" onClick={() => move(i, -1)} disabled={i === 0} title="Move up in order">↑</button>
                  <button className="adm-feat-btn" onClick={() => move(i, +1)} disabled={i === images.length - 1} title="Move down">↓</button>
                  <button className="adm-feat-btn danger" onClick={() => removeAt(i)} title="Remove">✕</button>
                </div>
              </div>
            ))}
          </div>
        </div>
      </section>
    );
  }

  window.AdminCatalogApp = RestoredAdminCatalogApp;
})();
