/* BB Crafting — crafting profit ledger (real ArcheAge data) */

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

// ─── Format helpers ─────────────────────────────────────────────────────
function fmtG(n) {
  if (!Number.isFinite(n)) return '—';
  const abs = Math.abs(n);
  const sign = n < 0 ? '−' : '';
  if (abs >= 1000) return sign + abs.toLocaleString(undefined, { maximumFractionDigits: 0 });
  if (abs >= 100)  return sign + abs.toFixed(0);
  if (abs >= 10)   return sign + abs.toFixed(1);
  if (abs >= 1)    return sign + abs.toFixed(2);
  if (abs >= 0.01) return sign + abs.toFixed(3);
  return sign + abs.toFixed(4);
}

function fmtInt(n) {
  return Number.isFinite(n) ? Math.round(n).toLocaleString() : '—';
}

// Compact integer formatter — e.g. 1234 -> "1.2k", 12345 -> "12k", 1234567 -> "1.2M".
// Used for daily volume to keep meta lines short.
function fmtCompact(n) {
  if (!Number.isFinite(n)) return '—';
  const a = Math.abs(n);
  if (a < 1000) return Math.round(n).toString();
  if (a < 10000) return (n / 1000).toFixed(1).replace(/\.0$/, '') + 'k';
  if (a < 1000000) return Math.round(n / 1000) + 'k';
  if (a < 10000000) return (n / 1000000).toFixed(1).replace(/\.0$/, '') + 'M';
  return Math.round(n / 1000000) + 'M';
}

function initialFor(name) {
  return (name || '').replace(/[^A-Za-z0-9]/g, '').slice(0, 2).toUpperCase() || '◇';
}

// ─── Cost tally (recursive) ─────────────────────────────────────────────
// Gilda Dust item id — used to filter out the "ranking" recipe variants
// across calculators. For every craftable item that has multiple recipes,
// there's typically a no-Gilda alternate with slightly fewer materials;
// profit-focused crafting always uses the no-Gilda version, so we strip
// Gilda variants from candidate lists whenever an alternate exists.
const GILDA_DUST_ID = '8000026';
function preferNoGilda(recipes) {
  if (!recipes || recipes.length <= 1) return recipes;
  const noGilda = recipes.filter(r => !r.materials.some(m => String(m.itemType) === GILDA_DUST_ID));
  return noGilda.length > 0 ? noGilda : recipes;
}

function getRecipeFor(itemType, ctx, preferred) {
  // preferred is per-itemType "recipe pick" map for items with multiple recipes
  let rs = ctx.recipesByProduct.get(itemType);
  if (!rs || rs.length === 0) return null;
  // Honor the hidden/blacklist set — hidden recipes shouldn't be used as
  // sub-crafts for ingredient calculations. If every recipe for this item is
  // hidden, fall back to "not craftable" so callers buy at market instead.
  if (ctx.hidden && ctx.hidden.size > 0) {
    const visible = rs.filter(r => !ctx.hidden.has(r.index));
    if (visible.length === 0) return null;
    rs = visible;
  }
  // Strip Gilda variants when alternates exist — see preferNoGilda comment.
  rs = preferNoGilda(rs);
  const idx = (preferred && preferred[itemType]) || 0;
  return rs[Math.min(idx, rs.length - 1)];
}

function tally(recipe, choices, priceOverrides, ctx, opts = {}) {
  const scale = opts.scale ?? 1;
  const depth = opts.depth ?? 0;
  const visited = opts.visited ?? new Set();
  const threshold = ctx.laborThreshold ?? 0;
  const laborMult = ctx.laborMult ?? 1;
  let cost = 0;
  let labor = (recipe.laborCost || 0) * scale * laborMult;
  const missing = [];

  for (const mat of recipe.materials) {
    const qty = mat.amount * scale;
    const key = mat.itemType;
    const subRecipe = getRecipeFor(key, ctx);
    const canCraft = !!subRecipe && depth < 6 && !visited.has(key);
    const choice = (choices[key] || defaultModeFor(key, qty, subRecipe, canCraft, ctx, choices, priceOverrides, depth, visited, threshold));

    if (choice === 'craft' && canCraft) {
      const newV = new Set(visited); newV.add(key);
      const subScale = qty / subRecipe.productCount;
      const sub = tally(subRecipe, choices, priceOverrides, ctx, { scale: subScale, depth: depth + 1, visited: newV });
      cost += sub.cost;
      labor += sub.labor;
      for (const m of sub.missing) missing.push(m);
    } else {
      const price = (priceOverrides[key] != null) ? priceOverrides[key] : (ctx.prices[key] ?? 0);
      cost += price * qty;
      if (ctx.prices[key] == null && priceOverrides[key] == null) missing.push(mat.name);
    }
  }
  return { cost, labor, missing };
}

// Default Buy vs Craft pick for an ingredient — used by both tally() and the
// IngredientRow display so they always agree. Rule: craft when the gold saved
// per labor (vs buying at market) meets the user's threshold. With no market
// price we have no choice but to craft; with no labor required we craft when
// it's strictly cheaper.
function defaultModeFor(key, qty, subRecipe, canCraft, ctx, choices, priceOverrides, depth, visited, threshold) {
  if (!canCraft) return 'market';
  const marketPrice = ctx.prices[key];
  if (marketPrice == null) return 'craft';
  // Compute sub-craft cost + labor to compare against buying outright.
  const newV = new Set(visited); newV.add(key);
  const subScale = qty / subRecipe.productCount;
  const sub = tally(subRecipe, choices, priceOverrides, ctx, { scale: subScale, depth: depth + 1, visited: newV });
  const marketCost = marketPrice * qty;
  const savings = marketCost - sub.cost;
  if (sub.labor <= 0) return savings > 0 ? 'craft' : 'market';
  const gpl = savings / sub.labor; // gold per labor for crafting vs buying
  return gpl >= threshold ? 'craft' : 'market';
}

// ─── Loading screen ─────────────────────────────────────────────────────
function Boot() {
  return (
    <div className="boot">
      <div className="boot-inner">
        <div className="boot-mark">⚜</div>
        <div className="boot-title">BB Crafting</div>
        <div className="boot-sub">Unfurling the ledger…</div>
      </div>
    </div>
  );
}

// ─── Sidebar ────────────────────────────────────────────────────────────
const PAGE = 200;

function Sidebar({ ctx, selectedIdx, compareIdx, onSelect, onToggleCompare, search, setSearch, sort, setSort, filter, setFilter, favs, onToggleFav, hidden, onToggleHidden, priceOverrides, sellOverrides, choices }) {
  const [visibleCount, setVisibleCount] = useState(PAGE);
  const filtersRef = useRef(null);

  // Click-and-drag horizontal scroll on the filter bar. We toggle a "dragging"
  // class for cursor swap and to suppress smooth-scroll while the pointer
  // moves. We also suppress the trailing click on the chip that initiated the
  // drag (so the user doesn't accidentally switch filters when they meant to
  // scroll), but allow a quick tap (<5px movement) to register as a click.
  useEffect(() => {
    const el = filtersRef.current;
    if (!el) return;
    let dragging = false;
    let startX = 0, startScroll = 0, moved = 0;
    const onDown = (e) => {
      if (e.button !== 0) return;
      dragging = true;
      moved = 0;
      startX = e.clientX;
      startScroll = el.scrollLeft;
      el.classList.add('dragging');
    };
    const onMove = (e) => {
      if (!dragging) return;
      const dx = e.clientX - startX;
      moved = Math.max(moved, Math.abs(dx));
      el.scrollLeft = startScroll - dx;
    };
    const onUp = () => {
      if (!dragging) return;
      dragging = false;
      el.classList.remove('dragging');
    };
    // Cancel a click on a chip if the pointer moved more than a few pixels.
    const onClickCapture = (e) => {
      if (moved > 5) {
        e.preventDefault();
        e.stopPropagation();
      }
    };
    el.addEventListener('mousedown', onDown);
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
    el.addEventListener('click', onClickCapture, true);
    return () => {
      el.removeEventListener('mousedown', onDown);
      window.removeEventListener('mousemove', onMove);
      window.removeEventListener('mouseup', onUp);
      el.removeEventListener('click', onClickCapture, true);
    };
  }, []);

  // Pre-compute profit/labor per recipe (this is heavy; memo)
  const filtered = useMemo(() => {
    const s = search.trim().toLowerCase();
    const list = [];
    for (let i = 0; i < ctx.recipes.length; i++) {
      const r = ctx.recipes[i];
      const isHidden = hidden.has(i);
      const isFav = favs.has(i);
      // Pinned items take priority over the hidden filter — otherwise a
      // pin-then-hide leaves the topbar counter > 0 with an empty list.
      if (filter === 'hidden') {
        if (!isHidden) continue;
      } else if (isHidden && !(filter === 'fav' && isFav)) {
        continue;
      }
      if (filter === 'fav' && !isFav) continue;
      if (filter === 'priced' && ctx.prices[r.productItemType] == null) continue;
      // Search relevance: product matches outrank material-only matches.
      // 3 = product exact, 2 = product starts-with, 1 = product contains,
      // 0 = material-only (still kept, just sorted below product hits).
      let matchScore = 0;
      if (s) {
        const pn = r.productName.toLowerCase();
        if (pn === s) matchScore = 3;
        else if (pn.startsWith(s)) matchScore = 2;
        else if (pn.includes(s)) matchScore = 1;
        else if (r.materials.some(m => m.name.toLowerCase().includes(s))) matchScore = 0;
        else continue; // no match anywhere
      }
      const sell = sellOverrides[i] != null ? sellOverrides[i] : (ctx.prices[r.productItemType] || 0);
      const t = tally(r, choices[i] || {}, priceOverrides[i] || {}, ctx);
      const profit = sell * r.productCount - t.cost;
      const hasPrice = ctx.prices[r.productItemType] != null || sellOverrides[i] != null;
      if (filter === 'profit' && (!hasPrice || profit <= 0)) continue;
      list.push({ idx: i, recipe: r, profit, labor: t.labor, hasPrice, matchScore });
    }
    list.sort((a, b) => {
      // When searching, always surface product-name matches above material-only hits.
      if (s && a.matchScore !== b.matchScore) return b.matchScore - a.matchScore;
      switch (sort) {
        case 'profit': return b.profit - a.profit;
        case 'profitlab': {
          const pa = a.labor > 0 ? a.profit / a.labor : a.profit;
          const pb = b.labor > 0 ? b.profit / b.labor : b.profit;
          return pb - pa;
        }
        case 'name':   return a.recipe.productName.localeCompare(b.recipe.productName);
        case 'labor':  return a.labor - b.labor;
        default: return 0;
      }
    });
    return list;
  }, [ctx, search, sort, filter, favs, hidden, priceOverrides, sellOverrides, choices]);

  useEffect(() => { setVisibleCount(PAGE); }, [search, sort, filter]);

  const visible = filtered.slice(0, visibleCount);

  return (
    <aside className="sidebar">
      <div className="sb-search">
        <div className="sb-search-field">
          <span className="sb-search-icon">
            <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
              <circle cx="7" cy="7" r="5" />
              <line x1="11" y1="11" x2="14" y2="14" />
            </svg>
          </span>
          <input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search recipes or ingredients…" />
          {search && (
            <button type="button" className="sb-search-clear" onClick={() => setSearch('')} aria-label="Clear search" title="Clear search">✕</button>
          )}
        </div>
      </div>

      <div className="sb-filters" ref={filtersRef}>
        <button className={`cat-chip ${filter==='all'?'active':''}`} onClick={() => setFilter('all')}>All</button>
        <button className={`cat-chip ${filter==='priced'?'active':''}`} onClick={() => setFilter('priced')}>Priced</button>
        <button className={`cat-chip ${filter==='profit'?'active':''}`} onClick={() => setFilter('profit')}>Profitable</button>
        <button className={`cat-chip ${filter==='fav'?'active':''}`} onClick={() => setFilter('fav')}>★ Pinned</button>
        {hidden.size > 0 && (
          <button className={`cat-chip ${filter==='hidden'?'active':''}`} onClick={() => setFilter('hidden')}>
            ⊘ Hidden · {hidden.size}
          </button>
        )}
      </div>

      <div className="sb-sort">
        <span>{filtered.length.toLocaleString()} recipe{filtered.length===1?'':'s'}</span>
        <div>
          <span style={{marginRight: 6, fontSize: 10, letterSpacing: '0.18em', textTransform: 'uppercase'}}>Sort</span>
          <select className="sort-select" value={sort} onChange={e => setSort(e.target.value)}>
            <option value="profit">Profit ↓</option>
            <option value="profitlab">Profit/labor ↓</option>
            <option value="name">Name A–Z</option>
            <option value="labor">Labor ↑</option>
          </select>
        </div>
      </div>

      <div className="sb-list">
        {visible.map(({ idx, recipe, profit, labor, hasPrice }) => {
          const isSel = idx === selectedIdx;
          const isCmp = idx === compareIdx;
          const isFav = favs.has(idx);
          const isHidden = hidden.has(idx);
          return (
            <div
              key={idx}
              className={`recipe-row ${isSel?'selected':''} ${isCmp?'compare':''} ${isHidden?'is-hidden':''}`}
              onClick={() => onSelect(idx)}
              onContextMenu={(e) => { e.preventDefault(); onToggleCompare(idx); }}
            >
              <div className="r-icon-wrap">
                <window.ItemIcon itemType={recipe.productItemType} name={recipe.productName} size="sm" />
              </div>
              <div style={{minWidth: 0}}>
                <div className="r-name">{recipe.productName}</div>
                <div className="r-sub">
                  <span>#{recipe.craftId}</span>
                  <span className="dot">·</span>
                  <span>{recipe.materials.length} mat{recipe.materials.length===1?'':'s'}</span>
                  <span className="dot">·</span>
                  <span style={{color: 'var(--labor)'}}>{recipe.laborCost} lp</span>
                </div>
              </div>
              <div className="r-right">
                {hasPrice ? (
                  <div className={`r-profit ${profit >= 0 ? 'pos' : 'neg'}`}>
                    {profit >= 0 ? '+' : '−'}{fmtG(profit)}g
                  </div>
                ) : (
                  <div className="r-profit nomk">no market</div>
                )}
                {labor > 0 && hasPrice && (
                  <div className="r-labor">{(profit/labor >= 0 ? '+' : '')}{fmtG(profit/labor)}g/lp</div>
                )}
              </div>
              <div className="row-actions">
                <button
                  className={`star-btn ${isFav ? 'on' : ''}`}
                  onClick={(e) => { e.stopPropagation(); onToggleFav(idx); }}
                  title={isFav ? 'Unpin' : 'Pin'}
                >{isFav ? '★' : '☆'}</button>
                <button
                  className={`hide-btn ${isHidden ? 'on' : ''}`}
                  onClick={(e) => { e.stopPropagation(); onToggleHidden(idx); }}
                  title={isHidden ? 'Restore' : 'Hide recipe'}
                >{isHidden ? '↩' : '✕'}</button>
              </div>
            </div>
          );
        })}
        {visible.length < filtered.length && (
          <button className="sb-loadmore" onClick={() => setVisibleCount(v => v + PAGE)}>
            Show {Math.min(PAGE, filtered.length - visibleCount)} more
          </button>
        )}
        {filtered.length === 0 && (
          <div style={{padding: '40px 20px', textAlign: 'center', color: 'var(--muted)', fontSize: 12}}>
            No recipes match your filters.
          </div>
        )}
      </div>
    </aside>
  );
}

window.Sidebar = Sidebar;
window.Boot = Boot;
window.tally = tally;
window.getRecipeFor = getRecipeFor;
window.defaultModeFor = defaultModeFor;
window.fmtG = fmtG;
window.fmtInt = fmtInt;
window.fmtCompact = fmtCompact;
window.initialFor = initialFor;
