/* Living Home editor — shared core: API, hooks, small components. Globals for the other files. */
const { useState, useEffect, useMemo, useRef, useCallback, createContext, useContext } = React;
const API = '/api/home-admin';
const STORE = (window.__PC_STORE_URL || 'https://premierecollectibles.com');

async function api(path, opts = {}) {
  const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), opts.timeout || 25000);
  try {
    const r = await fetch(API + path, { credentials: 'include', headers: { 'Content-Type': 'application/json', ...(opts.headers || {}) }, signal: ctrl.signal, ...opts, body: opts.body && typeof opts.body !== 'string' ? JSON.stringify(opts.body) : opts.body });
    clearTimeout(t);
    const j = await r.json().catch(() => ({}));
    if (r.status === 401) { window.dispatchEvent(new CustomEvent('home:unauth')); return { error: j.error || 'Signed out', status: 401 }; }
    if (!r.ok) return { error: j.error || `HTTP ${r.status}`, status: r.status, ...j };
    return j;
  } catch (e) { clearTimeout(t); return { error: e.name === 'AbortError' ? 'Timed out' : e.message }; }
}
const ToastCtx = createContext(() => {});
const useToast = () => useContext(ToastCtx);
function ToastHost({ children }) {
  const [msg, setMsg] = useState(null); const timer = useRef();
  const show = useCallback((text, kind) => { setMsg({ text, kind }); clearTimeout(timer.current); timer.current = setTimeout(() => setMsg(null), 3800); }, []);
  return <ToastCtx.Provider value={show}>{children}{msg && <div className={'toast ' + (msg.kind || '')}>{msg.text}</div>}</ToastCtx.Provider>;
}
const DraftCtx = createContext(null);
const useDraft = () => useContext(DraftCtx);

const ARRIVALS = [
  { key: 'new', label: 'New visitor', params: 'pc_demo=new' }, { key: 'returning', label: 'Returning', params: 'pc_demo=returning' }, { key: 'email', label: 'Email click', params: 'utm_medium=email&utm_source=klaviyo' },
  { key: 'social', label: 'Social', params: 'fbclid=preview' }, { key: 'paid', label: 'Paid search + signer', params: 'gclid=preview&utm_term=ozzy+osbourne' }, { key: 'member', label: 'Signed-in member', params: 'pc_demo=member' }, { key: 'live', label: 'Auction week', params: 'pc_demo=live' },
];
const SEGMENT_FIELDS = [
  { key: 'source', label: 'Arrival source', type: 'enum', options: ['email', 'paid_search', 'social', 'search', 'referral', 'direct'] },
  { key: 'returning', label: 'Returning visitor', type: 'bool' }, { key: 'signed_in', label: 'Signed in', type: 'bool' },
  { key: 'customer_group', label: 'BigCommerce customer group', type: 'text' },
  { key: 'affinity', label: 'Affinity (viewed lane ≥ N)', type: 'lane' },
  { key: 'device', label: 'Device', type: 'enum', options: ['mobile', 'desktop'] },
  { key: 'season', label: 'Season active', type: 'text' }, { key: 'auction_week', label: 'Auction week (live lots)', type: 'bool' },
  { key: 'klaviyo_segment', label: 'Klaviyo segment (kseg= in link)', type: 'text' }, { key: 'hour', label: 'Hour of day (0–23)', type: 'range' },
  { key: 'intent', label: 'Has a search intent', type: 'bool' },
];
const LANES = ['books', 'vinyl', 'sports', 'music', 'movies_tv', 'historic', 'exclusives'];
const QUERY_FIELDS = [
  { key: 'q', label: 'Search words / signer' }, { key: 'filter.custom_collection', label: 'Collection (Sports, Music, Movies…)' }, { key: 'filter.custom_genre', label: 'Genre (Horror, Rock…)' },
  { key: 'filter.custom_sport', label: 'Sport' }, { key: 'filter.categories_hierarchy', label: 'Category (Signed Books, Signed Vinyl…)' }, { key: 'filter.brand', label: 'Signer (exact brand)' },
  { key: 'filter.price.high', label: 'Price cap ($)' }, { key: 'filter.price.low', label: 'Price floor ($)' }, { key: 'filter.ss_is_preorder', label: 'Pre-orders only (1)' },
  { key: 'sort.total_sold', label: 'Sort: bestselling (desc)' }, { key: 'sort.ss_days_since_created', label: 'Sort: newest (asc)' },
];

function Field({ label, hint, children, count, max }) {
  return <div className="field"><div className="row"><label>{label}</label>{count != null && <span className={'count right ' + (max && count > max ? 'over' : '')}>{count}{max ? ' / ' + max : ''}</span>}</div>{children}{hint && <span className="hint">{hint}</span>}</div>;
}
function Text({ value, onChange, max, ...rest }) { return <input type="text" value={value || ''} onChange={e => onChange(e.target.value)} maxLength={max || undefined} {...rest} />; }
function Toggle({ on, onChange, label }) { return <span className={'toggle ' + (on ? 'on' : '')} onClick={() => onChange(!on)} role="switch" aria-checked={!!on}><i /> {label && <span>{label}</span>}</span>; }
function Pill({ kind, children }) { return <span className={'pill ' + (kind || '')}>{children}</span>; }
function Modal({ title, onClose, children, wide, footer }) {
  useEffect(() => { const k = e => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', k); return () => window.removeEventListener('keydown', k); }, []);
  return <div className="modal-bg" onMouseDown={e => { if (e.target === e.currentTarget) onClose(); }}><div className={'modal ' + (wide ? 'wide' : '')}><div className="row" style={{ marginBottom: 12 }}><h2>{title}</h2><button className="btn ghost right" onClick={onClose}>✕</button></div>{children}{footer && <div className="row" style={{ marginTop: 16, justifyContent: 'flex-end' }}>{footer}</div>}</div></div>;
}
function Confirm({ text, onYes, onNo, yes = 'Confirm' }) { return <Modal title="Are you sure?" onClose={onNo} footer={<><button className="btn" onClick={onNo}>Cancel</button><button className="btn primary" onClick={onYes}>{yes}</button></>}><p>{text}</p></Modal>; }

function useDebouncedEffect(fn, deps, ms) { const first = useRef(true); useEffect(() => { if (first.current) { first.current = false; return; } const t = setTimeout(fn, ms); return () => clearTimeout(t); }, deps); }
function fmt(n, d = 0) { return Number(n || 0).toLocaleString(undefined, { maximumFractionDigits: d }); }
function money(n) { return '$' + Number(n || 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); }
function pct(n) { return (Number(n || 0) * 100).toFixed(1) + '%'; }
function ago(iso) { if (!iso) return ''; const s = (Date.now() - new Date(iso).getTime()) / 1000; if (s < 60) return 'just now'; if (s < 3600) return Math.floor(s / 60) + ' min ago'; if (s < 86400) return Math.floor(s / 3600) + ' h ago'; return Math.floor(s / 86400) + ' d ago'; }
function slug(s) { return String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40); }

/* Searchspring query builder: key/value rows + live proof */
function QueryBuilder({ value, onChange, proof = true, signer }) {
  const q = value || {};
  const rows = Object.entries(q);
  const [check, setCheck] = useState(null); const [busy, setBusy] = useState(false);
  const set = (k, v) => { const n = { ...q }; if (v === '' || v == null) delete n[k]; else n[k] = v; onChange(n); };
  const add = () => { const free = QUERY_FIELDS.find(f => !(f.key in q)); if (free) set(free.key, free.key.startsWith('sort.') ? (free.key === 'sort.total_sold' ? 'desc' : 'asc') : ''); };
  const run = async () => { setBusy(true); const r = await api('/seasons/proof', { method: 'POST', body: signer ? { signer } : { query: q } }); setBusy(false); setCheck(r.data || { error: r.error }); };
  return <div className="stack">
    {rows.map(([k, v]) => <div className="row" key={k}>
      <select value={k} onChange={e => { const n = { ...q }; delete n[k]; n[e.target.value] = v; onChange(n); }} style={{ width: 280 }}>{QUERY_FIELDS.map(f => <option key={f.key} value={f.key}>{f.label}</option>)}</select>
      <input type="text" className="grow" value={Array.isArray(v) ? v.join(' | ') : v} onChange={e => set(k, e.target.value.includes(' | ') ? e.target.value.split(' | ').map(s => s.trim()) : e.target.value)} placeholder="value (use ' | ' for several)" />
      <button className="btn sm" onClick={() => set(k, null)}>Remove</button>
    </div>)}
    <div className="row"><button className="btn sm" onClick={add} type="button">+ Add a condition</button>{proof && <button className="btn sm gold" type="button" onClick={run} disabled={busy}>{busy ? 'Checking…' : 'Check against the catalog'}</button>}{check && !check.error && <span className={'pill ' + (check.total < 8 ? 'warn' : 'good')}>{fmt(check.total)} in stock</span>}{check && check.error && <span className="pill bad">{check.error}</span>}</div>
    {check && check.items && <Tiles items={check.items.slice(0, 8)} />}
    {check && check.facets && <div className="row wrap small muted">{check.facets.map(f => <span key={f.field}><b>{f.field.replace('filter.', '').replace('custom_', '')}:</b> {f.values.slice(0, 6).map(v => v.value + ' (' + v.count + ')').join(', ')}</span>)}</div>}
  </div>;
}
function Tiles({ items, onPick, selected }) {
  return <div className="tiles">{(items || []).map(p => <div className={'tile ' + (selected && selected.has(String(p.id)) ? 'selected' : '')} key={p.id} onClick={() => onPick && onPick(p)} style={{ cursor: onPick ? 'pointer' : 'default' }}>
    <div className="img" style={{ backgroundImage: `url("${p.img}")` }} /><div className="b">{p.brand && <div className="brand">{p.brand}</div>}<div className="name">{p.name}</div><div>{p.price ? money(p.price) : ''}{p.pre ? <Pill kind="gold">pre-order</Pill> : ''}{p.stock != null && p.stock <= 3 ? <span className="muted"> · {p.stock} left</span> : ''}</div>{p.rule && <Pill kind={p.rule === 'searchspring' ? '' : 'navy'}>{p.rule.replace('_', ' ')}</Pill>}</div></div>)}</div>;
}
function ProductSearch({ onPick, placeholder = 'Search the catalog by signer or title…' }) {
  const [q, setQ] = useState(''); const [items, setItems] = useState([]); const [busy, setBusy] = useState(false);
  useDebouncedEffect(async () => { if (q.trim().length < 2) return setItems([]); setBusy(true); const r = await api(`/products/search?q=${encodeURIComponent(q)}&n=12`); setBusy(false); setItems(r.data || []); }, [q], 300);
  return <div className="stack"><input type="search" value={q} onChange={e => setQ(e.target.value)} placeholder={placeholder} />{busy && <span className="muted small">Searching…</span>}<Tiles items={items} onPick={onPick} /></div>;
}
function AssetPicker({ value, onChange, assets, reload }) {
  const [open, setOpen] = useState(false);
  const a = (assets || []).find(x => x.id === value);
  return <div className="row">
    {a ? <div className="asset" style={{ width: 120 }}><div className="img" style={{ backgroundImage: `url("${a.sizes['320'] || a.sizes.orig}")` }} /><div className={'meta ' + (a.alt ? '' : 'noalt')}>{a.alt || 'no alt text'}</div></div> : <span className="muted">No image</span>}
    <button className="btn sm" type="button" onClick={() => setOpen(true)}>Choose image</button>{a && <button className="btn sm" type="button" onClick={() => onChange(null)}>Clear</button>}
    {open && <Modal title="Image library" wide onClose={() => setOpen(false)}><AssetGrid assets={assets} reload={reload} onPick={x => { onChange(x.id); setOpen(false); }} /></Modal>}
  </div>;
}
function AssetGrid({ assets, reload, onPick, selectedId }) {
  const toast = useToast(); const [busy, setBusy] = useState(false); const fileRef = useRef();
  const upload = async (files) => {
    for (const f of files) {
      if (!/^image\//.test(f.type)) { toast(`${f.name}: not an image`, 'bad'); continue; }
      setBusy(true);
      const data = await new Promise(res => { const r = new FileReader(); r.onload = () => res(r.result); r.readAsDataURL(f); });
      const alt = prompt(`Alt text for ${f.name} (required before publish)`) || '';
      const r = await api('/assets', { method: 'POST', body: { name: f.name, mime: f.type, data, alt }, timeout: 60000 });
      setBusy(false);
      if (r.error) toast(r.error, 'bad'); else { toast(`Uploaded ${f.name}`); reload && reload(); }
    }
  };
  return <div className="stack">
    <div className="row"><input type="file" accept="image/*" multiple ref={fileRef} onChange={e => upload([...e.target.files])} style={{ display: 'none' }} /><button className="btn primary" onClick={() => fileRef.current.click()} disabled={busy}>{busy ? 'Uploading…' : 'Upload images'}</button><span className="muted small">JPEG, PNG, WebP, GIF or AVIF up to 8 MB. Resized to 320 / 640 / 1280 on upload. Alt text is required before publish.</span></div>
    <div className="assets">{(assets || []).map(a => <AssetCard key={a.id} a={a} onPick={onPick} selected={selectedId === a.id} reload={reload} />)}</div>
  </div>;
}
function AssetCard({ a, onPick, selected, reload }) {
  const [alt, setAlt] = useState(a.alt || ''); const toast = useToast();
  const saveAlt = async () => { if (alt === (a.alt || '')) return; const r = await api(`/assets/${a.id}`, { method: 'PUT', body: { alt } }); if (r.error) toast(r.error, 'bad'); else reload && reload(); };
  const focal = async (e) => { const r = e.currentTarget.getBoundingClientRect(); const x = (e.clientX - r.left) / r.width, y = (e.clientY - r.top) / r.height; await api(`/assets/${a.id}`, { method: 'PUT', body: { focal_x: +x.toFixed(3), focal_y: +y.toFixed(3) } }); reload && reload(); };
  return <div className={'asset ' + (selected ? 'selected' : '')}>
    <div className="img" style={{ backgroundImage: `url("${a.sizes['320'] || a.sizes.orig}")` }} onClick={e => onPick ? onPick(a) : focal(e)} title={onPick ? 'Use this image' : 'Click to set the focal point'}><span className="focal" style={{ left: (a.focal_x * 100) + '%', top: (a.focal_y * 100) + '%' }} /></div>
    <div className="meta"><input type="text" value={alt} onChange={e => setAlt(e.target.value)} onBlur={saveAlt} placeholder="Alt text (required)" style={{ padding: 4, fontSize: 11.5 }} /><div className="muted tiny">{a.width ? `${a.width}×${a.height}` : ''} · {fmt(a.bytes / 1024)} KB</div></div>
  </div>;
}
function Bars({ series, valueKey = 'views', labelKey = 'day' }) {
  const max = Math.max(1, ...series.map(s => s[valueKey] || 0));
  return <div className="bars">{series.map((s, i) => <div key={i} style={{ height: (100 * (s[valueKey] || 0) / max) + '%' }} data-l={`${s[labelKey]}: ${fmt(s[valueKey])}`} />)}</div>;
}
function PhonePreview({ kicker, title, sub }) { return <div className="phone-preview"><div className="k">{kicker || ' '}</div><div className="t" dangerouslySetInnerHTML={{ __html: title || 'America’s largest autograph store.' }} /><div className="s">{sub || ''}</div></div>; }
function Table({ cols, rows, empty = 'Nothing yet' }) {
  return <div className="table-wrap"><table><thead><tr>{cols.map(c => <th key={c.k} className={c.num ? 'num' : ''}>{c.l}</th>)}</tr></thead><tbody>{rows.length ? rows.map((r, i) => <tr key={r.id || r.key || i}>{cols.map(c => <td key={c.k} className={c.num ? 'num' : ''}>{c.f ? c.f(r) : r[c.k]}</td>)}</tr>) : <tr><td colSpan={cols.length} className="muted">{empty}</td></tr>}</tbody></table></div>;
}
