/* Detail panel + compare + main App + Tweaks */

const { useState: useStateD, useMemo: useMemoD, useEffect: useEffectD } = React;

// ─── Tweaks defaults (editable; persisted by host) ────────────────────────
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "currency": "g",
  "animScale": 1,
  "writeWebhookUrl": "",
  "adminPassword": "BBOT2345TEST"
}/*EDITMODE-END*/;

// Labor value (g/labor) is a USER preference persisted to localStorage, not
// EDITMODE — it's the threshold for default Craft vs Buy and lives in the top
// banner so it's always visible. Default 0.25g/labor.
const LABOR_KEY = 'craft.laborValue.v1';
function loadLaborValue() {
  try {
    const raw = localStorage.getItem(LABOR_KEY);
    if (raw == null) return 0.25;
    const v = parseFloat(raw);
    return Number.isFinite(v) && v >= 0 ? v : 0.25;
  } catch (e) { return 0.25; }
}

// Famed proficiency — ArcheAge buff that reduces labor cost by 40%.
const FAMED_KEY = 'craft.famed.v1';
function loadFamed() {
  try { return localStorage.getItem(FAMED_KEY) === '1'; } catch (e) { return false; }
}

// Crafting proficiency tier — replaces the simple "Famed yes/no" toggle with
// the full ArcheAge ladder. Each tier reduces labor cost by a fixed %, with
// colored display names matched to the in-game proficiency UI.
const PROFICIENCY_TIERS = [
  { id: 'novice',    name: 'Novice',    reduction: 0,  color: '#d6d6d6' },
  { id: 'veteran',   name: 'Veteran',   reduction: 5,  color: '#6dd4c5' },
  { id: 'expert',    name: 'Expert',    reduction: 10, color: '#b5e36b' },
  { id: 'master',    name: 'Master',    reduction: 15, color: '#f0d864' },
  { id: 'authority', name: 'Authority', reduction: 20, color: '#f29c44' },
  { id: 'champion',  name: 'Champion',  reduction: 20, color: '#f06854' },
  { id: 'adept',     name: 'Adept',     reduction: 20, color: '#f04866' },
  { id: 'herald',    name: 'Herald',    reduction: 20, color: '#ec4682' },
  { id: 'virtuoso',  name: 'Virtuoso',  reduction: 25, color: '#c149aa' },
  { id: 'celebrity', name: 'Celebrity', reduction: 30, color: '#9648c5' },
  { id: 'famed',     name: 'Famed',     reduction: 40, color: '#d4a960' },
];
const PROF_BY_ID = Object.fromEntries(PROFICIENCY_TIERS.map(t => [t.id, t]));
const PROF_KEY = 'craft.proficiency.v1';
function loadProficiency() {
  try {
    const v = localStorage.getItem(PROF_KEY);
    if (v && PROF_BY_ID[v]) return v;
    // Fallback to legacy famed boolean — preserves the old toggle's effect.
    if (localStorage.getItem(FAMED_KEY) === '1') return 'famed';
    return 'novice';
  } catch (e) { return 'novice'; }
}

// Per-recipe-idx state — user's manual Buy/Craft toggles, price overrides,
// sell-price overrides. Stored as { [idx]: { ... } } except sellOverrides which
// is { [idx]: number }. The recipe index is stable across reloads because
// recipes.json is shipped with the build; if the dataset ever changes, stale
// entries are harmless (referenced item types still resolve).
const PRICE_OVERRIDES_KEY = 'craft.priceOverrides.v1';
const SELL_OVERRIDES_KEY  = 'craft.sellOverrides.v1';
const CHOICES_KEY         = 'craft.choices.v1';
function loadObj(key) {
  try {
    const raw = localStorage.getItem(key);
    return raw ? JSON.parse(raw) : {};
  } catch (e) { return {}; }
}
function saveObj(key, obj) {
  try { localStorage.setItem(key, JSON.stringify(obj)); } catch (e) {}
}

// ─── Admin helpers ────────────────────────────────────────────────────────
const ADMIN_HIDDEN_KEY = 'craft.adminHidden.v1';
function loadAdminHidden() {
  try { const s = localStorage.getItem(ADMIN_HIDDEN_KEY); return s ? new Set(JSON.parse(s)) : new Set(); } catch { return new Set(); }
}

// ─── Admin login modal ────────────────────────────────────────────────────
function AdminLoginModal({ open, adminPassword, onClose, onSuccess }) {
  const [pw, setPw] = useStateD('');
  const [err, setErr] = useStateD('');
  if (!open) return null;
  function attempt(e) {
    e.preventDefault();
    if (!adminPassword) { setErr('No admin password configured — set one in Tweaks first.'); return; }
    if (pw === adminPassword) { onSuccess(); setPw(''); setErr(''); onClose(); }
    else { setErr('Incorrect password.'); setPw(''); }
  }
  return (
    <div className="admin-modal-overlay" onClick={onClose}>
      <form className="admin-modal-box" onClick={e => e.stopPropagation()} onSubmit={attempt}>
        <div className="admin-modal-title">Admin Access</div>
        <div className="admin-modal-sub">Recipe curation tools</div>
        <input
          type="password"
          className="admin-modal-input"
          placeholder="Password"
          value={pw}
          onChange={e => setPw(e.target.value)}
          autoFocus
        />
        {err && <div className="admin-modal-err">{err}</div>}
        <div className="admin-modal-actions">
          <button type="submit" className="btn">Enter</button>
          <button type="button" className="btn" style={{opacity: 0.6}} onClick={onClose}>Cancel</button>
        </div>
      </form>
    </div>
  );
}

// ─── Admin panel ──────────────────────────────────────────────────────────
function AdminPanel({ ctx, adminHiddenCraftIds, onToggle, onLogout, currency, choices, priceOverrides, sellOverrides }) {
  const profitable = useMemoD(() => {
    if (!ctx) return [];
    const out = [];
    for (let i = 0; i < ctx.recipes.length; i++) {
      const r = ctx.recipes[i];
      const marketPrice = ctx.prices[r.productItemType];
      if (marketPrice == null) continue;
      const sellUnit = (sellOverrides && sellOverrides[i] != null) ? sellOverrides[i] : marketPrice;
      const t = tally(r, (choices && choices[i]) || {}, (priceOverrides && priceOverrides[i]) || {}, ctx);
      const profit = sellUnit * r.productCount - t.cost;
      if (profit > 0) out.push({ r, i, profit });
    }
    return out.sort((a, b) => b.profit - a.profit);
  }, [ctx, choices, priceOverrides, sellOverrides]);

  const adminHiddenList = useMemoD(() => {
    if (!ctx) return [];
    return ctx.recipes.filter(r => adminHiddenCraftIds.has(r.craftId));
  }, [ctx, adminHiddenCraftIds]);

  function copyHidden() {
    const ids = [...adminHiddenCraftIds].sort((a, b) => a - b);
    try { navigator.clipboard.writeText(JSON.stringify(ids, null, 2)); } catch {}
  }

  return (
    <div className="admin-panel">
      <div className="page-hero">
        <div className="page-hero-crest">⚿</div>
        <div className="page-hero-meta">
          <div className="page-hero-eyebrow">ADMIN PANEL</div>
          <div className="page-hero-title">Recipe Curation</div>
          <div className="page-hero-tags">
            <span className="pill">{adminHiddenList.length} admin-hidden</span>
            <span className="dot" />
            <span>{profitable.length} profitable visible</span>
          </div>
        </div>
        <div className="page-hero-aside">
          <div className="admin-aside-btns">
            <button className="btn" onClick={copyHidden} title="Copy craftIds to clipboard — paste into data/default-hidden.json to make permanent">
              Copy craftIds
            </button>
            <button className="btn" style={{opacity: 0.7}} onClick={onLogout}>Log out</button>
          </div>
        </div>
      </div>

      {adminHiddenList.length > 0 && (
        <div className="framed-section" style={{marginBottom: 22}}>
          <div className="framed-section-head">
            <span className="fs-eyebrow">Hidden</span>
            <span className="fs-title">Admin-Hidden</span>
            <span className="fs-rule" />
            <span className="fs-aside">{adminHiddenList.length} entries · click ↩ to restore</span>
          </div>
          <div className="framed-section-body">
            <div className="admin-table-head">
              <span>Name</span><span>ID</span><span />
            </div>
            {adminHiddenList.map(r => (
              <div key={r.craftId ?? r.index} className="admin-row admin-row-dim">
                <span className="admin-row-name">{r.productName}</span>
                <span className="admin-row-id">#{r.craftId}</span>
                <button className="admin-restore-btn" onClick={() => onToggle(r.craftId)}>↩ Restore</button>
              </div>
            ))}
          </div>
        </div>
      )}

      <div className="framed-section">
        <div className="framed-section-head">
          <span className="fs-eyebrow">Curation</span>
          <span className="fs-title">Profitable Recipes</span>
          <span className="fs-rule" />
          <span className="fs-aside">{profitable.length} recipes · ✕ to hide from Codex</span>
        </div>
        <div className="framed-section-body">
          <div className="admin-table-head admin-table-head-4">
            <span>Profit</span><span>Name</span><span>ID</span><span />
          </div>
          {profitable.map(({ r, profit }) => (
            <div key={r.index} className={`admin-row admin-row-4 ${adminHiddenCraftIds.has(r.craftId) ? 'admin-row-xd' : ''}`}>
              <span className="admin-row-profit pos">+{fmtG(profit)}<span className="admin-cur">{currency}</span></span>
              <span className="admin-row-name">{r.productName}</span>
              <span className="admin-row-id">#{r.craftId}</span>
              {adminHiddenCraftIds.has(r.craftId)
                ? <button className="admin-restore-btn" onClick={() => onToggle(r.craftId)}>↩</button>
                : <button className="admin-x-btn" onClick={() => onToggle(r.craftId)}>✕</button>
              }
            </div>
          ))}
        </div>
      </div>

      <div className="admin-export-note">
        To make hidden recipes permanent for <em>all users</em>: click <strong>Copy craftIds</strong> above, then append
        the IDs into <code>data/default-hidden.json</code> → craftIds array and redeploy to Cloudflare Pages.
      </div>
    </div>
  );
}

// ─── Detail ──────────────────────────────────────────────────────────────
function Detail({ ctx, idx, recipe, priceOverrides, sellOverride, choices, expansion, onPriceChange, onResetPrice, onSellChange, onResetSell, onToggleMode, onToggleExpand, onToggleCompare, compareActive, onToggleFav, isFav, onJumpToRecipe, onOpenShopping, currency, laborValue }) {
  const tot = useMemoD(
    () => recipe ? tally(recipe, choices, priceOverrides, ctx) : { cost: 0, labor: 0, missing: [] },
    [recipe, choices, priceOverrides, ctx]
  );

  if (!recipe) {
    return (
      <main className="detail">
        <div className="empty-state">
          <div>
            <div className="emp-mark">⚒</div>
            <div className="emp-title">Open the Codex</div>
            <div className="emp-sub">Pick a recipe from the ledger on the left to tally cost, labor, and clean profit. Right-click any row to set it as comparison.</div>
          </div>
        </div>
      </main>
    );
  }

  const marketPrice = ctx.prices[recipe.productItemType];
  const sellUnit = sellOverride != null ? sellOverride : (marketPrice ?? 0);
  const sellTotal = sellUnit * recipe.productCount;

  const laborGold = tot.labor * (laborValue || 0);
  const profit = sellTotal - tot.cost;
  const isLoss = profit < 0;
  const margin = sellTotal > 0 ? (profit / sellTotal) * 100 : 0;
  const profitPerLabor = tot.labor > 0 ? (sellTotal - tot.cost) / tot.labor : 0;

  // Composition segments (top-level materials only)
  const threshold = ctx.laborThreshold ?? 0;
  const segments = recipe.materials.map(mat => {
    const key = mat.itemType;
    const subRecipe = getRecipeFor(key, ctx);
    const canCraft = !!subRecipe;
    const qty = mat.amount;
    const choice = (choices[key] || window.defaultModeFor(key, qty, subRecipe, canCraft, ctx, choices, priceOverrides, 0, new Set(), threshold));
    let value;
    if (choice === 'craft' && canCraft) {
      const sub = tally(subRecipe, choices, priceOverrides, ctx, { scale: mat.amount / subRecipe.productCount, depth: 1, visited: new Set([key]) });
      value = sub.cost;
    } else {
      const price = (priceOverrides[key] != null) ? priceOverrides[key] : (ctx.prices[key] ?? 0);
      value = price * mat.amount;
    }
    return { name: mat.name, value };
  });
  const totalSeg = segments.reduce((s, x) => s + x.value, 0);
  const segColors = ['#d4a960', '#5b9bd5', '#6fb96a', '#b07cd1', '#d56a6a', '#e6b370', '#9aa3b2', '#e8d8b6', '#7a6747'];

  return (
    <main className="detail" key={idx}>
      <div className="fade-in">
        <div className="r-header">
          <div className="r-crest"><window.ItemIcon itemType={recipe.productItemType} name={recipe.productName} size="lg" /></div>
          <div className="r-meta">
            <div className="r-cat">Recipe № {recipe.craftId} · Item {recipe.productItemType}</div>
            <h1 className="r-title">{recipe.productName}</h1>
            <div className="r-tags">
              <span className="pill">×{recipe.productCount} per craft</span>
              <span className="dot" />
              <span>{recipe.materials.length} ingredient{recipe.materials.length===1?'':'s'}</span>
              <span className="dot" />
              <span style={{color: 'var(--labor)'}}>{recipe.laborCost} labor</span>
              {(() => {
                const vol = (ctx.volumes && ctx.volumes[recipe.productItemType]);
                if (vol == null) return null;
                const unit = vol.p === 'week' ? '/wk' : '/day';
                const label = vol.p === 'week' ? 'Weekly auction volume' : 'Daily auction volume';
                return (
                  <>
                    <span className="dot" />
                    <span className={`vol-tag ${vol.p === 'week' ? 'weekly' : ''}`} title={`${label}: ${fmtInt(vol.v)} units${unit === '/wk' ? '/week' : '/day'}`}>
                      <span className="vol-ico">↻</span>
                      {fmtCompact(vol.v)}{unit}
                    </span>
                  </>
                );
              })()}
              {marketPrice == null && (
                <>
                  <span className="dot" />
                  <span style={{color: 'var(--garnet)'}}>No market price recorded</span>
                </>
              )}
              {tot.missing.length > 0 && (
                <>
                  <span className="dot" />
                  <span style={{color: 'var(--garnet)'}}>
                    {tot.missing.length} ingredient{tot.missing.length===1?'':'s'} missing price
                  </span>
                </>
              )}
            </div>
          </div>
          <div className="r-actions">
            <button className="btn" onClick={onOpenShopping} title="Show shopping list for materials to buy based on current Buy/Craft picks">⌬ Shopping list</button>
            <button className="btn" onClick={onToggleFav}>{isFav ? '★ Pinned' : '☆ Pin'}</button>
            <button className={`btn ${compareActive ? 'active' : ''}`} onClick={onToggleCompare}>
              {compareActive ? '⇆ Comparing' : '⇆ Compare'}
            </button>
          </div>
        </div>

        <div className="numbers-strip">
          <div className={`metric ${isLoss ? 'loss' : 'profit'}`}>
            <div className="lbl">Profit per craft</div>
            <div className="val">
              <span className="tally-tick" key={profit.toFixed(4)}>{profit >= 0 ? '' : '−'}{fmtG(Math.abs(profit))}</span>
              <span className="currency">{currency}</span>
            </div>
            <div className="sub">
              {marketPrice == null && sellOverride == null
                ? 'Set a sell price to compute'
                : isLoss
                  ? 'You are losing copper'
                  : `${margin.toFixed(1)}% margin · ${fmtG(profitPerLabor)}${currency}/labor`}
            </div>
          </div>
          <div className="metric">
            <div className="lbl">Material cost</div>
            <div className="val" style={{color: 'var(--parch)'}}>
              <span>{fmtG(tot.cost)}</span>
              <span className="currency">{currency}</span>
            </div>
            <div className="sub">{recipe.materials.length} stacks</div>
          </div>
          <div className="metric labor">
            <div className="lbl">Labor</div>
            <div className="val">
              <span className="lp">{fmtInt(tot.labor)}</span>
              <span style={{fontSize: 12, color: 'var(--labor)', opacity: 0.7, marginLeft: 4}}>lp</span>
            </div>
            <div className="sub" style={{color: 'var(--labor)', opacity: 0.7}}>
              {laborValue > 0 ? `≈ ${fmtG(laborGold)}${currency} @ ${laborValue}${currency}/lp` : 'No gold weight set'}
            </div>
          </div>
          <div className="metric sale">
            <div className="lbl">Sale total</div>
            <div className="val" style={{color: 'var(--gold)'}}>
              <span>{fmtG(sellTotal)}</span>
              <span className="currency">{currency}</span>
            </div>
            <div className="sale-controls">
              <span className={`price-edit ${sellOverride != null ? 'modified' : ''}`} title="Per-unit sale price">
                <input
                  type="number"
                  step="0.01"
                  value={sellUnit}
                  min="0"
                  onChange={(e) => onSellChange(e.target.value === '' ? 0 : Number(e.target.value))}
                />
                <span className="cur">{currency}</span>
              </span>
              {recipe.productCount > 1 && (
                <span className="sale-x">× {recipe.productCount}</span>
              )}
              {sellOverride != null ? (
                <span className={`sale-delta ${marketPrice != null && sellUnit - marketPrice >= 0 ? 'pos' : 'neg'}`}>
                  {marketPrice != null
                    ? <>{sellUnit - marketPrice >= 0 ? '+' : '−'}{fmtG(Math.abs(sellUnit - marketPrice))} vs market</>
                    : 'custom'}
                  <span className="price-reset" onClick={onResetSell}>reset</span>
                </span>
              ) : marketPrice != null ? (
                <span className="sale-tag">market avg</span>
              ) : (
                <span className="sale-tag none">not listed</span>
              )}
            </div>
          </div>
        </div>

        {/* Cost composition */}
        <div className="compose">
          <div className="section-head" style={{margin: '0 0 12px'}}>
            <span style={{fontSize: 12, letterSpacing: '0.22em', fontFamily: 'var(--sans)', textTransform: 'uppercase', color: 'var(--muted)', fontWeight: 600}}>Cost Composition</span>
            <span className="ruler" />
          </div>
          <div className="compose-bar">
            {segments.map((s, i) => {
              const pct = totalSeg > 0 ? (s.value / totalSeg) * 100 : 0;
              return (
                <div
                  key={s.name + i}
                  className="compose-seg"
                  style={{ width: pct + '%', background: segColors[i % segColors.length] }}
                  title={`${s.name}: ${pct.toFixed(1)}% (${fmtG(s.value)}${currency})`}
                />
              );
            })}
          </div>
          <div className="compose-legend">
            {segments.map((s, i) => {
              const pct = totalSeg > 0 ? (s.value / totalSeg) * 100 : 0;
              return (
                <span key={s.name + i}>
                  <span className="sw" style={{ background: segColors[i % segColors.length] }} />
                  {s.name}<span className="pct">{pct.toFixed(1)}%</span>
                </span>
              );
            })}
          </div>
        </div>

        {/* Ingredients tree */}
        <div className="section-head">
          <span className="name">Ingredients</span>
          <span className="num">×{recipe.materials.length}</span>
          <span className="ruler" />
          <span style={{fontSize: 10, color: 'var(--muted)', letterSpacing: '0.18em', textTransform: 'uppercase'}}>
            Toggle <span style={{color: 'var(--gold)'}}>Buy</span> / <span style={{color: 'var(--sapphire)'}}>Craft</span> per ingredient
          </span>
        </div>

        <div className="ing-tree">
          <div className="ing-tree-head">
            <div>Ingredient</div>
            <div>Qty</div>
            <div>Unit price · source</div>
            <div>Subtotal</div>
          </div>
          {recipe.materials.map((mat, i) => (
            <IngredientRow
              key={i + ':' + mat.itemType}
              mat={mat}
              scale={1}
              depth={0}
              ctx={ctx}
              choices={choices}
              priceOverrides={priceOverrides}
              expansion={expansion}
              onToggleExpand={onToggleExpand}
              onToggleMode={onToggleMode}
              onPriceChange={onPriceChange}
              onResetPrice={onResetPrice}
              onJumpToRecipe={onJumpToRecipe}
              currency={currency}
              parentKey="root"
            />
          ))}
          <div className="tree-totals">
            <div className="lbl">Total cost (incl. sub-crafts)</div>
            <div className="stack">
              <span className="v labor">{fmtInt(tot.labor)} <small>labor</small></span>
              <span className="v cost">−{fmtG(tot.cost)}{currency} <small>materials</small></span>
            </div>
          </div>
        </div>

        <div style={{height: compareActive ? 96 : 24}} />
      </div>
    </main>
  );
}

// ─── Compare bar ─────────────────────────────────────────────────────────
function CompareBar({ ctx, a, aIdx, b, bIdx, priceOverrides, sellOverrides, choices, laborValue, onClose, currency }) {
  function compute(r, idx) {
    if (!r) return { profit: 0 };
    const marketPrice = ctx.prices[r.productItemType];
    const sellUnit = sellOverrides[idx] != null ? sellOverrides[idx] : (marketPrice ?? 0);
    const t = tally(r, choices[idx] || {}, priceOverrides[idx] || {}, ctx);
    return { profit: sellUnit * r.productCount - t.cost };
  }
  const A = compute(a, aIdx);
  const B = compute(b, bIdx);
  const aWins = a && b && A.profit > B.profit;
  const bWins = a && b && B.profit > A.profit;

  return (
    <div className="compare-bar">
      <button className="cmp-close" onClick={onClose}>Close ✕</button>
      <div className="cmp-slot">
        {a ? (
          <>
            <div style={{minWidth: 0}}>
              <div style={{fontSize: 9, letterSpacing: '0.28em', color: 'var(--muted)', textTransform: 'uppercase'}}>Recipe A</div>
              <div className="cmp-name">{a.productName}</div>
              {aWins && <div className="cmp-winner">▲ More profitable</div>}
            </div>
            <div className={`cmp-profit ${A.profit >= 0 ? 'pos' : 'neg'}`} style={{marginLeft: 'auto'}}>{A.profit >= 0 ? '+' : '−'}{fmtG(Math.abs(A.profit))}{currency}</div>
          </>
        ) : <div className="cmp-empty">Select recipe</div>}
      </div>
      <div className="cmp-vs">VS</div>
      <div className="cmp-slot right">
        {b ? (
          <>
            <div className={`cmp-profit ${B.profit >= 0 ? 'pos' : 'neg'}`}>{B.profit >= 0 ? '+' : '−'}{fmtG(Math.abs(B.profit))}{currency}</div>
            <div style={{minWidth: 0}}>
              <div style={{fontSize: 9, letterSpacing: '0.28em', color: 'var(--muted)', textTransform: 'uppercase'}}>Recipe B</div>
              <div className="cmp-name">{b.productName}</div>
              {bWins && <div className="cmp-winner">▲ More profitable</div>}
            </div>
          </>
        ) : <div className="cmp-empty">Right-click any recipe to compare</div>}
      </div>
    </div>
  );
}

// Shared refresher — used by both Tweaks panel and Prices panel.
async function doRefreshFromSheet(setFeedState, onPricesUpdated) {
  setFeedState({ state: 'loading', msg: 'Fetching prices…' });
  try {
    const result = await window.fetchLivePrices();
    const count = Object.keys(result.prices).length;
    if (count === 0) throw new Error('No prices parsed from sheet');
    onPricesUpdated(result.prices, count, result);
    setFeedState({
      state: 'live',
      msg: `7d avg · ${result.lastUpdated || 'just now'}`,
      lastUpdated: result.lastUpdated,
    });
  } catch (err) {
    setFeedState({ state: 'error', msg: 'Failed: ' + err.message });
  }
}

// ─── Tweaks UI + sheet refresher ─────────────────────────────────────────
function TweaksUI({ onPricesUpdated, feedState, setFeedState }) {
  const { TweaksPanel, useTweaks, TweakSection, TweakSlider, TweakRadio, TweakButton, TweakText } = window;
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);

  useEffectD(() => {
    document.documentElement.style.setProperty('--anim-scale', t.animScale);
    window.dispatchEvent(new CustomEvent('tweak-change', { detail: t }));
  }, [t.currency, t.animScale, t.writeWebhookUrl]);

  const refreshFromSheet = () => doRefreshFromSheet(setFeedState, onPricesUpdated);

  return (
    <TweaksPanel title="Tweaks">
      <TweakSection label="Currency">
        <TweakRadio
          label="Symbol"
          value={t.currency}
          onChange={v => setTweak('currency', v)}
          options={[
            { value: 'g',  label: 'Gold' },
            { value: '⊙', label: 'Solari' },
            { value: '✦', label: 'Star' },
            { value: '⌬', label: 'Aether' },
          ]}
        />
      </TweakSection>

      <TweakSection label="Live prices">
        <TweakText
          label="Write webhook (Apps Script)"
          value={t.writeWebhookUrl}
          onChange={v => setTweak('writeWebhookUrl', v)}
        />
        <TweakButton label="Refresh from sheet" onClick={refreshFromSheet} />
        <div style={{fontSize: 11, color: 'var(--muted)', marginTop: 8, lineHeight: 1.5}}>
          Reads 7-day averages from the shared price sheet (30-day fallback).
          Paste an Apps Script URL above to write your manual prices back — see <span style={{color: 'var(--gold)'}}>SETUP_WEBHOOK.md</span>.
        </div>
        <div style={{fontSize: 11, color: 'var(--muted)', marginTop: 8, lineHeight: 1.5}}>
          The labor value (g/labor) lives in the top banner — it drives the Buy/Craft default for every ingredient.
        </div>
      </TweakSection>

      <TweakSection label="Motion">
        <TweakSlider
          label="Animation intensity"
          value={t.animScale}
          onChange={v => setTweak('animScale', v)}
          min={0} max={2} step={0.1}
          unit="×"
        />
      </TweakSection>

      <TweakSection label="Admin">
        <TweakText
          label="Admin password"
          value={t.adminPassword || ''}
          onChange={v => setTweak('adminPassword', v)}
        />
        <div style={{fontSize: 11, color: 'var(--muted)', marginTop: 8, lineHeight: 1.5}}>
          Set a password here, then use the Admin tab to hide recipes from the Codex profitable list.
        </div>
      </TweakSection>
    </TweaksPanel>
  );
}

// ─── Root App ────────────────────────────────────────────────────────────
function App() {
  const [ctx, setCtx] = useStateD(null);
  const [view, setView] = useStateD('codex'); // 'codex' | 'prices'
  const [manualPrices, setManualPrices] = useStateD(() => window.loadManualPrices());
  const [search, setSearch] = useStateD('');
  const [sort, setSort] = useStateD('profit');
  const [filter, setFilter] = useStateD('priced');
  const [selectedIdx, setSelectedIdx] = useStateD(null);
  const [compareIdx, setCompareIdx] = useStateD(null);
  const [favs, setFavs] = useStateD(() => {
    try { const s = localStorage.getItem('craft.favs.v1'); return s ? new Set(JSON.parse(s)) : new Set(); } catch { return new Set(); }
  });
  const [hidden, setHidden] = useStateD(() => {
    try { const s = localStorage.getItem('craft.hidden.v1'); return s ? new Set(JSON.parse(s)) : new Set(); } catch { return new Set(); }
  });
  // Recipes default-hidden via data/default-hidden.json that the user has
  // explicitly un-hidden — overrides the default for them only. Persisted.
  const [userUnhidden, setUserUnhidden] = useStateD(() => {
    try { const s = localStorage.getItem('craft.unhidden.v1'); return s ? new Set(JSON.parse(s)) : new Set(); } catch { return new Set(); }
  });
  const [priceOverrides, setPriceOverrides] = useStateD(() => loadObj(PRICE_OVERRIDES_KEY));  // per-recipe-idx -> { itemType: price }
  const [sellOverrides, setSellOverrides]   = useStateD(() => loadObj(SELL_OVERRIDES_KEY));   // per-recipe-idx -> price
  const [choices, setChoices]               = useStateD(() => loadObj(CHOICES_KEY));         // per-recipe-idx -> { itemType: 'market'|'craft' }
  const [expansion, setExpansion] = useStateD({});            // per-recipe-idx -> { key: bool } — ephemeral, no need to persist

  const [adminHiddenCraftIds, setAdminHiddenCraftIds] = useStateD(loadAdminHidden);
  const [isAdmin, setIsAdmin] = useStateD(() => sessionStorage.getItem('craft.isAdmin') === '1');
  const [adminLoginOpen, setAdminLoginOpen] = useStateD(false);
  const [adminPassword, setAdminPassword] = useStateD(TWEAK_DEFAULTS.adminPassword);

  const [currency, setCurrency] = useStateD(TWEAK_DEFAULTS.currency);
  const [laborValue, setLaborValueState] = useStateD(loadLaborValue);
  const setLaborValue = (v) => {
    const clamped = Math.max(0, Number(v) || 0);
    setLaborValueState(clamped);
    try { localStorage.setItem(LABOR_KEY, String(clamped)); } catch (e) {}
  };
  const [famed, setFamedState] = useStateD(loadFamed);
  const setFamed = (on) => {
    const v = !!on;
    setFamedState(v);
    try { localStorage.setItem(FAMED_KEY, v ? '1' : '0'); } catch (e) {}
  };
  const [proficiency, setProficiencyState] = useStateD(loadProficiency);
  const setProficiency = (id) => {
    if (!PROF_BY_ID[id]) return;
    setProficiencyState(id);
    try { localStorage.setItem(PROF_KEY, id); } catch (e) {}
  };
  const profTier = PROF_BY_ID[proficiency] || PROF_BY_ID.novice;
  const laborMultEff = 1 - profTier.reduction / 100;
  const [webhookUrl, setWebhookUrl] = useStateD(TWEAK_DEFAULTS.writeWebhookUrl);
  const [feedState, setFeedState] = useStateD({ state: 'loading', msg: 'Loading prices…' });
  const [toast, setToast] = useStateD(null);
  const [shoppingOpen, setShoppingOpen] = useStateD(false);

  useEffectD(() => {
    window.CRAFT_LOADER.then(data => {
      setCtx(data);
      setFeedState(data.priceMeta);
      // Prune stale fav / hidden indices that no longer exist in the dataset
      // (they leave the topbar counters > 0 with nothing to show).
      const n = data.recipes.length;
      setFavs(prev => {
        const next = new Set([...prev].filter(i => Number.isFinite(i) && i >= 0 && i < n));
        return next.size === prev.size ? prev : next;
      });
      setHidden(prev => {
        const next = new Set([...prev].filter(i => Number.isFinite(i) && i >= 0 && i < n));
        return next.size === prev.size ? prev : next;
      });
      // pre-select first profitable recipe
      let firstIdx = 0;
      for (let i = 0; i < data.recipes.length; i++) {
        const r = data.recipes[i];
        if (data.prices[r.productItemType] != null) { firstIdx = i; break; }
      }
      setSelectedIdx(firstIdx);
    });
  }, []);

  useEffectD(() => {
    const handler = (e) => {
      setCurrency(e.detail.currency);
      setWebhookUrl(e.detail.writeWebhookUrl);
      setAdminPassword(e.detail.adminPassword || '');
    };
    window.addEventListener('tweak-change', handler);
    return () => window.removeEventListener('tweak-change', handler);
  }, []);

  useEffectD(() => {
    if (!toast) return;
    const t = setTimeout(() => setToast(null), 3500);
    return () => clearTimeout(t);
  }, [toast]);

  // Persist manual prices on change
  useEffectD(() => {
    window.saveManualPrices(manualPrices);
  }, [manualPrices]);

  // Persist Buy/Craft picks + price overrides + sell overrides.
  useEffectD(() => { saveObj(PRICE_OVERRIDES_KEY, priceOverrides); }, [priceOverrides]);
  useEffectD(() => { saveObj(SELL_OVERRIDES_KEY,  sellOverrides);  }, [sellOverrides]);
  useEffectD(() => { saveObj(CHOICES_KEY,         choices);        }, [choices]);

  // Persist favs + hidden
  useEffectD(() => {
    try { localStorage.setItem('craft.favs.v1', JSON.stringify([...favs])); } catch {}
  }, [favs]);
  useEffectD(() => {
    try { localStorage.setItem('craft.hidden.v1', JSON.stringify([...hidden])); } catch {}
  }, [hidden]);
  useEffectD(() => {
    try { localStorage.setItem('craft.unhidden.v1', JSON.stringify([...userUnhidden])); } catch {}
  }, [userUnhidden]);
  useEffectD(() => {
    try { localStorage.setItem(ADMIN_HIDDEN_KEY, JSON.stringify([...adminHiddenCraftIds])); } catch {}
  }, [adminHiddenCraftIds]);

  // Effective hidden set = defaults ∪ user-hidden ∪ admin-hidden, minus user-explicitly-unhidden.
  // Defaults live in data/default-hidden.json and are computed once at load.
  const effHidden = useMemoD(() => {
    if (!ctx) return new Set();
    const def = ctx.defaultHidden || new Set();
    const result = new Set([...def, ...hidden]);
    for (const i of userUnhidden) result.delete(i);
    // Admin-hidden: stored by craftId, converted to recipe index here.
    if (adminHiddenCraftIds.size > 0) {
      for (const r of ctx.recipes) {
        if (adminHiddenCraftIds.has(r.craftId)) result.add(r.index);
      }
    }
    return result;
  }, [ctx, hidden, userUnhidden, adminHiddenCraftIds]);

  // Effective ctx: sheet prices < manual prices < hardcoded prices.
  // Also carries laborThreshold so tally() + IngredientRow + cost-composition
  // segments all default Buy/Craft against the same g/labor target.
  // Hidden recipes are passed through so getRecipeFor() can skip them as
  // sub-craft candidates — effectively a per-recipe blacklist.
  const effCtx = useMemoD(() => {
    if (!ctx) return null;
    return {
      ...ctx,
      prices: { ...ctx.prices, ...manualPrices, ...window.HARDCODED_PRICES },
      volumes: ctx.volumes || {},
      laborThreshold: laborValue,
      laborMult: laborMultEff,
      hidden: effHidden,
    };
  }, [ctx, manualPrices, laborValue, laborMultEff, effHidden]);

  // Top stats — must be defined before early return to keep hook order stable
  const pinnedProfit = useMemoD(() => {
    if (!effCtx) return 0;
    let s = 0;
    for (const i of favs) {
      const r = effCtx.recipes[i];
      if (!r) continue;
      const marketPrice = effCtx.prices[r.productItemType];
      if (marketPrice == null && sellOverrides[i] == null) continue;
      const sellUnit = sellOverrides[i] != null ? sellOverrides[i] : marketPrice;
      const t = tally(r, choices[i] || {}, priceOverrides[i] || {}, effCtx);
      s += sellUnit * r.productCount - t.cost;
    }
    return s;
  }, [favs, effCtx, priceOverrides, sellOverrides, choices, laborValue]);

  if (!effCtx) return <Boot />;

  const recipe = selectedIdx != null ? effCtx.recipes[selectedIdx] : null;
  const compareRecipe = compareIdx != null ? effCtx.recipes[compareIdx] : null;

  function onSelect(i) { setSelectedIdx(i); }
  function onToggleCompare(i) {
    setCompareIdx(c => c === i ? null : i);
  }
  function onToggleFav(i) {
    setFavs(prev => { const n = new Set(prev); if (n.has(i)) n.delete(i); else n.add(i); return n; });
  }
  function onToggleHidden(i) {
    const isHiddenNow = effHidden.has(i);
    const isDefault = ctx && ctx.defaultHidden && ctx.defaultHidden.has(i);
    if (isHiddenNow) {
      // Make it visible
      if (isDefault) setUserUnhidden(prev => { const n = new Set(prev); n.add(i); return n; });
      setHidden(prev => { const n = new Set(prev); n.delete(i); return n; });
      // If hiding the currently-selected recipe is being un-hidden, no action.
    } else {
      // Make it hidden
      setUserUnhidden(prev => { const n = new Set(prev); n.delete(i); return n; });
      setHidden(prev => { const n = new Set(prev); n.add(i); return n; });
      // If hiding the currently-selected recipe, jump to next visible.
      if (selectedIdx === i && effCtx) {
        for (let k = 0; k < effCtx.recipes.length; k++) {
          if (!effHidden.has(k) && k !== i) { setSelectedIdx(k); break; }
        }
      }
    }
  }
  function onPriceChange(itemType, value) {
    setPriceOverrides(prev => ({ ...prev, [selectedIdx]: { ...(prev[selectedIdx] || {}), [itemType]: value } }));
  }
  function onResetPrice(itemType) {
    setPriceOverrides(prev => {
      const c = { ...(prev[selectedIdx] || {}) }; delete c[itemType];
      return { ...prev, [selectedIdx]: c };
    });
  }
  function onSellChange(v) {
    setSellOverrides(prev => ({ ...prev, [selectedIdx]: v }));
  }
  function onResetSell() {
    setSellOverrides(prev => { const c = { ...prev }; delete c[selectedIdx]; return c; });
  }
  function onToggleMode(itemType, mode) {
    setChoices(prev => ({ ...prev, [selectedIdx]: { ...(prev[selectedIdx] || {}), [itemType]: mode } }));
  }
  function onToggleExpand(key) {
    setExpansion(prev => ({ ...prev, [selectedIdx]: { ...(prev[selectedIdx] || {}), [key]: !((prev[selectedIdx] || {})[key]) } }));
  }
  function onAdminHideToggle(craftId) {
    setAdminHiddenCraftIds(prev => {
      const n = new Set(prev);
      if (n.has(craftId)) n.delete(craftId); else n.add(craftId);
      return n;
    });
  }
  function onAdminLogout() {
    sessionStorage.removeItem('craft.isAdmin');
    setIsAdmin(false);
    setView('codex');
  }
  function onAdminLogin() {
    sessionStorage.setItem('craft.isAdmin', '1');
    setIsAdmin(true);
    setView('admin');
  }

  function onJumpToRecipe(itemType) {
    // Find the first recipe whose product matches this itemType; jump to it.
    const idx = effCtx.recipes.findIndex(r => r.productItemType === itemType);
    if (idx >= 0) {
      setSelectedIdx(idx);
      // Scroll detail back to top so the user sees the new recipe's header.
      requestAnimationFrame(() => {
        const detail = document.querySelector('.detail');
        if (detail) detail.scrollTop = 0;
      });
    }
  }
  function onPricesUpdated(newPrices, count, result) {
    setCtx(c => ({
      ...c,
      prices:  { ...c.prices,  ...newPrices },
      volumes: { ...(c.volumes || {}), ...((result && result.volumes) || {}) },
    }));
    setToast({ kind: 'ok', text: `Updated ${count} prices from sheet` });
  }

  return (
    <>
      <div className={`app ${view === 'codex' ? '' : 'no-sidebar'}`} data-view={view}>
        <header className="topbar">
          <div className="brand">
            <div className="brand-mark">⚜</div>
            <div>
              <div className="brand-name">BB Crafting</div>
              <div className="brand-sub">ArcheAge Crafting Ledger</div>
            </div>
          </div>
          <div style={{flex: 1}} />
          <div className="view-tabs">
            <button className={`view-tab ${view === 'codex' ? 'active' : ''}`} onClick={() => setView('codex')}>
              <span>⚒</span> Codex
            </button>
            <button className={`view-tab ${view === 'prices' ? 'active' : ''}`} onClick={() => setView('prices')}>
              <span>⚖</span> Prices
              {Object.keys(manualPrices).length > 0 && <span className="badge">{Object.keys(manualPrices).length}</span>}
            </button>
            <button className={`view-tab ${view === 'wisps' ? 'active' : ''}`} onClick={() => setView('wisps')}>
              <span>⚛</span> Wisps
            </button>
            <button className={`view-tab ${view === 'seeds' ? 'active' : ''}`} onClick={() => setView('seeds')}>
              <span>⚘</span> Seeds
            </button>
            <button className={`view-tab ${view === 'regrade' ? 'active' : ''}`} onClick={() => setView('regrade')}>
              <span>⚙</span> Regrade
            </button>
            <div className="view-tab-sep" />
            <button
              className={`view-tab view-tab-admin ${view === 'admin' ? 'active' : ''} ${isAdmin ? 'admin-authed' : ''}`}
              onClick={() => isAdmin ? setView('admin') : setAdminLoginOpen(true)}
              title={isAdmin ? 'Open admin panel' : 'Admin login'}
            >
              <span>{isAdmin ? '⚿' : '⚿'}</span> Admin
            </button>
          </div>
          <div className="topbar-stat">
            <div className="topbar-stat-label">Recipes</div>
            <div className="topbar-stat-val">{effCtx.recipes.length.toLocaleString()}</div>
          </div>
          <button
            type="button"
            className={`topbar-stat clickable ${view === 'codex' && filter === 'fav' ? 'active' : ''}`}
            title={favs.size > 0 ? 'Filter sidebar to pinned recipes' : 'No pinned recipes yet'}
            onClick={() => {
              if (favs.size === 0) return;
              setView('codex');
              setFilter('fav');
            }}
            disabled={favs.size === 0}
          >
            <div className="topbar-stat-label">Pinned</div>
            <div className="topbar-stat-val">{favs.size}</div>
          </button>
          <button
            type="button"
            className={`topbar-stat clickable ${view === 'codex' && filter === 'fav' ? 'active' : ''}`}
            title={favs.size > 0 ? 'Filter sidebar to pinned recipes' : 'No pinned recipes yet'}
            onClick={() => {
              if (favs.size === 0) return;
              setView('codex');
              setFilter('fav');
            }}
            disabled={favs.size === 0}
          >
            <div className="topbar-stat-label">Pinned profit</div>
            <div className={`topbar-stat-val ${pinnedProfit >= 0 ? 'pos' : 'neg'}`}>
              {pinnedProfit >= 0 ? '+' : '−'}{fmtG(Math.abs(pinnedProfit))}{currency}
            </div>
          </button>
        </header>

        <div className="settings-strip">
          <div className="ss-feed">
            <span className={`feed-dot ${feedState.state}`} />
            <span className="ss-feed-msg">{feedState.msg}</span>
          </div>
          <div className="ss-spacer" />
          <label className="ss-prof" title="Crafting proficiency — reduces labor cost. Saved locally.">
            <span className="ss-prof-label">Proficiency</span>
            <select
              className="ss-prof-select"
              value={proficiency}
              onChange={(e) => setProficiency(e.target.value)}
              style={{ color: profTier.color, borderColor: profTier.color + '66' }}
            >
              {PROFICIENCY_TIERS.map(t => (
                <option
                  key={t.id}
                  value={t.id}
                  style={{ color: t.color, background: '#0d1322' }}
                >
                  {t.name}  ·  −{t.reduction}% labor
                </option>
              ))}
            </select>
          </label>
          <div className="ss-threshold" title="Default Buy vs Craft threshold — for each craftable ingredient, the app picks Craft when the gold saved per labor (vs buying at market) meets this value, else Buy. Saved locally.">
            <span className="ss-threshold-label">g/labor threshold</span>
            <div className="threshold-control compact">
              <button
                className="thr-step"
                onClick={() => setLaborValue(Math.max(0, +(laborValue - 0.05).toFixed(4)))}
                aria-label="Decrease threshold"
              >−</button>
              <input
                type="number"
                className="thr-input"
                value={laborValue}
                min="0"
                step="0.05"
                onChange={(e) => setLaborValue(e.target.value === '' ? 0 : Number(e.target.value))}
              />
              <span className="thr-unit">{currency}/lp</span>
              <button
                className="thr-step"
                onClick={() => setLaborValue(+(laborValue + 0.05).toFixed(4))}
                aria-label="Increase threshold"
              >+</button>
            </div>
          </div>
        </div>

        {view === 'codex' ? (
          <>
            <Sidebar
              ctx={effCtx}
              selectedIdx={selectedIdx}
              compareIdx={compareIdx}
              onSelect={onSelect}
              onToggleCompare={onToggleCompare}
              search={search} setSearch={setSearch}
              sort={sort} setSort={setSort}
              filter={filter} setFilter={setFilter}
              favs={favs} onToggleFav={onToggleFav}
              hidden={effHidden} onToggleHidden={onToggleHidden}
              priceOverrides={priceOverrides}
              sellOverrides={sellOverrides}
              choices={choices}
            />

            <Detail
              ctx={effCtx}
              idx={selectedIdx}
              recipe={recipe}
              priceOverrides={priceOverrides[selectedIdx] || {}}
              sellOverride={sellOverrides[selectedIdx]}
              choices={choices[selectedIdx] || {}}
              expansion={expansion[selectedIdx] || {}}
              onPriceChange={onPriceChange}
              onResetPrice={onResetPrice}
              onSellChange={onSellChange}
              onResetSell={onResetSell}
              onToggleMode={onToggleMode}
              onToggleExpand={onToggleExpand}
              onToggleCompare={() => onToggleCompare(selectedIdx)}
              compareActive={compareIdx != null}
              onToggleFav={() => onToggleFav(selectedIdx)}
              isFav={favs.has(selectedIdx)}
              onJumpToRecipe={onJumpToRecipe}
              onOpenShopping={() => setShoppingOpen(true)}
              currency={currency}
              laborValue={laborValue}
            />

            {compareIdx != null && (
              <CompareBar
                ctx={effCtx}
                a={recipe} aIdx={selectedIdx}
                b={compareRecipe} bIdx={compareIdx}
                priceOverrides={priceOverrides}
                sellOverrides={sellOverrides}
                choices={choices}
                laborValue={laborValue}
                onClose={() => setCompareIdx(null)}
                currency={currency}
              />
            )}
          </>
        ) : view === 'prices' ? (
          React.createElement(window.PricesPanel, {
            ctx: effCtx,
            sheetPrices: ctx.prices,
            manualPrices: manualPrices,
            setManualPrices: setManualPrices,
            currency: currency,
            webhookUrl: webhookUrl,
            feedState: feedState,
            refreshFromSheet: () => doRefreshFromSheet(setFeedState, onPricesUpdated),
          })
        ) : view === 'wisps' ? (
          React.createElement(window.WispPanel, {
            ctx: effCtx,
            laborValue: laborValue,
            currency: currency,
          })
        ) : view === 'seeds' ? (
          React.createElement(window.SeedsPanel, {
            ctx: effCtx,
            laborValue: laborValue,
            currency: currency,
          })
        ) : view === 'regrade' ? (
          React.createElement(window.RegradePanel, {
            ctx: effCtx,
            laborValue: laborValue,
            currency: currency,
          })
        ) : (
          <AdminPanel
            ctx={effCtx}
            adminHiddenCraftIds={adminHiddenCraftIds}
            onToggle={onAdminHideToggle}
            onLogout={onAdminLogout}
            currency={currency}
            choices={choices}
            priceOverrides={priceOverrides}
            sellOverrides={sellOverrides}
          />
        )}
      </div>
      <TweaksUI onPricesUpdated={onPricesUpdated} feedState={feedState} setFeedState={setFeedState} />
      {recipe && (
        <window.ShoppingListModal
          open={shoppingOpen}
          onClose={() => setShoppingOpen(false)}
          recipe={recipe}
          ctx={effCtx}
          choices={choices[selectedIdx] || {}}
          priceOverrides={priceOverrides[selectedIdx] || {}}
          currency={currency}
          laborValue={laborValue}
        />
      )}
      <AdminLoginModal
        open={adminLoginOpen}
        adminPassword={adminPassword}
        onClose={() => setAdminLoginOpen(false)}
        onSuccess={onAdminLogin}
      />
      {toast && <div className={`toast ${toast.kind}`}>{toast.text}</div>}
    </>
  );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
