// Mesh Button Builder — main components

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

// ---------- Helpers ----------
const cx = (...a) => a.filter(Boolean).join(' ');
// Dedupe by name. When the same name appears multiple times (e.g. Coinbase
// has both an exchange and a ramp variant), keep the highest-ranked one.
const uniqByName = (arr) => {
  const best = new Map();
  for (const i of arr) {
    const cur = best.get(i.name);
    if (!cur || rankOf(i) < rankOf(cur)) best.set(i.name, i);
  }
  // Preserve original input order on ties to avoid surprising callers that
  // already sorted; just substitute each name with its best representative.
  const seen = new Set();
  const out = [];
  for (const i of arr) {
    if (seen.has(i.name)) continue;
    seen.add(i.name);
    out.push(best.get(i.name));
  }
  return out;
};

// Pick the right icon/logo variant for a given surface.
// Rule: prefer Color (full-brand) on neutral surfaces; otherwise pick the
// variant whose NAME means "for use on this surface" — Light = for light bg,
// Dark = for dark bg. Falls back gracefully through the chain.
//   surface: 'light' | 'dark' | 'brand' (e.g. button using brand color)
const pickIcon = (i, surface = 'light') => {
  if (!i) return '';
  if (surface === 'brand') {
    // On a colored brand button, prefer the icon designed for that brand
    // (usually the Light variant which is the "primary" mark).
    return i.iconLight || i.iconColor || i.iconDark || '';
  }
  if (surface === 'dark') {
    return i.iconDark || i.iconColor || i.iconLight || '';
  }
  return i.iconColor || i.iconLight || i.iconDark || '';
};
const pickLogo = (i, surface = 'light') => {
  if (!i) return '';
  if (surface === 'brand') return i.logoLight || i.logoColor || i.logoDark || '';
  if (surface === 'dark') return i.logoDark || i.logoColor || i.logoLight || '';
  return i.logoColor || i.logoLight || i.logoDark || '';
};

// Authoritative ordering for integrations.
// rank.js exposes window.RANK_BY_ID (lower number = higher priority).
// Unranked integrations sort to the end, alphabetically.
const rankOf = (i) => {
  const r = (typeof window !== 'undefined' && window.RANK_BY_ID) ? window.RANK_BY_ID[i.id] : undefined;
  return (r === undefined) ? Infinity : r;
};
const byRank = (a, b) => {
  const ra = rankOf(a), rb = rankOf(b);
  if (ra !== rb) return ra - rb;
  return a.name.localeCompare(b.name);
};

const CATEGORY_LABELS = {
  exchange: 'Exchanges & Brokerages',
  deFiWallet: 'Self-Custody Wallets',
};

const CATEGORY_DESCRIPTIONS = {
  exchange: 'Centralized exchanges and brokerages — Coinbase, Binance, Kraken, Robinhood, etc.',
  deFiWallet: 'Self-custody and hardware wallets — MetaMask, Phantom, Ledger, etc.',
};

// Default button copy per deposit lifecycle (Smart/Easy/Category) — shared by
// the live preview, embed-code generators, and customizer placeholder text.
function defaultDepositLabel({ mode, integration, category }) {
  if (mode === 'single' && integration) return `Deposit with ${integration.name}`;
  if (mode === 'multi') return 'Direct deposit';
  if (mode === 'category') {
    if (category === 'exchange') return 'Deposit from an exchange';
    if (category === 'deFiWallet') return 'Deposit from a wallet';
    return 'Deposit';
  }
  return 'Deposit with Mesh';
}

// Returns default subtext for a button based on mode (used in button preview).
function defaultDepositSubtext({ mode }) {
  if (mode === 'multi') return 'Securely deposit to the correct address';
  if (mode === 'single') return 'Deposit again in one tap';
  return '';
}

// ---------- Icons ----------
const Icon = {
  Search: ({ size = 16 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.3-4.3"></path></svg>
  ),
  Check: ({ size = 16 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
  ),
  Copy: ({ size = 14 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path></svg>
  ),
  ChevronRight: ({ size = 16 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m9 18 6-6-6-6"></path></svg>
  ),
  ChevronLeft: ({ size = 16 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m15 18-6-6 6-6"></path></svg>
  ),
  X: ({ size = 14 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>
  ),
  Sparkle: ({ size = 14 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z"></path></svg>
  ),
  Plus: ({ size = 14 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14"></path><path d="M12 5v14"></path></svg>
  ),
  Code: ({ size = 16 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="16 18 22 12 16 6"></polyline><polyline points="8 6 2 12 8 18"></polyline></svg>
  ),
  Download: ({ size = 14 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
  ),
  Card: ({ size = 14 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="6" width="18" height="13" rx="2"/><path d="M3 10h18"/></svg>
  ),
  Exchange: ({ size = 14 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 7h13l-3-3"/><path d="M21 17H8l3 3"/></svg>
  ),
  Phone: ({ size = 14 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="6" y="3" width="12" height="18" rx="2"/><path d="M11 18h2"/></svg>
  ),
  Checkout: ({ size = 14 }) => (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="9" cy="20" r="1.5"/><circle cx="17" cy="20" r="1.5"/><path d="M3 4h2l2.5 11h11l2-7H6"/></svg>
  ),
  Mesh: ({ size = 24 }) => (
    <svg width={size} height={size} viewBox="0 0 32 32" fill="none">
      <rect width="32" height="32" rx="8" fill="#0F172A"/>
      <path d="M9 22V10l7 6 7-6v12" stroke="white" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"/>
    </svg>
  ),
};

// ---------- Step indicator ----------
function Steps({ current, steps }) {
  return (
    <div className="steps">
      {steps.map((s, i) => (
        <React.Fragment key={s}>
          <div className={cx('step', i === current && 'step-active', i < current && 'step-done')}>
            <div className="step-dot">{i < current ? <Icon.Check size={12}/> : i + 1}</div>
            <span>{s}</span>
          </div>
          {i < steps.length - 1 && <div className={cx('step-line', i < current && 'step-line-done')}/>}
        </React.Fragment>
      ))}
    </div>
  );
}

// ---------- Mode picker ----------
// IDs stay as 'multi'/'single'/'category' under the hood — the relabeling is
// purely a framing layer, mapping each technical mode to a deposit lifecycle
// stage from Mesh's deposit UX best-practice playbook.
const MODES = [
  {
    id: 'multi',
    title: 'Direct deposit',
    eyebrow: 'First-time users',
    desc: 'Authenticate once, deposit forever. A single button opens a curated chooser of your top providers — Coinbase, Binance, Metamask, and more.',
    bestFor: 'Your default deposit CTA',
    preview: 'multi',
  },
  {
    id: 'single',
    title: 'Quick deposit',
    eyebrow: 'Returning users',
    desc: 'One-click from already linked accounts. Each provider gets its own dedicated button — "Coinbase", "Metamask" — for the fastest repeat flow.',
    bestFor: 'Returning-user deposit page',
    preview: 'single',
  },
  {
    id: 'category',
    title: 'Category Deposit',
    eyebrow: 'Scoped flows',
    desc: 'Filter the full Mesh catalog to a category — wallets, exchanges, or DeFi. Best for contextual prompts like "Connect a wallet".',
    bestFor: 'Contextual entry points',
    preview: 'category',
  },
];

function ModeCard({ mode, active, disabled, onClick, sample }) {
  return (
    <button className={cx('mode-card', active && 'mode-card-active', disabled && 'mode-card-disabled')} onClick={onClick} disabled={disabled}>
      <div className="mode-preview">
        <ModePreview type={mode.preview} sample={sample}/>
      </div>
      <div className="mode-body">
        {mode.eyebrow && <div className="mode-eyebrow">{mode.eyebrow}</div>}
        <div className="mode-title">{mode.title}</div>
        <div className="mode-desc">{mode.desc}</div>
        {mode.bestFor && <div className="mode-bestfor">Best for: <span>{mode.bestFor}</span></div>}
      </div>
      <div className="mode-radio">
        <div className="mode-radio-inner"/>
      </div>
    </button>
  );
}

function ModePreview({ type, sample }) {
  if (type === 'single') {
    // Standalone preview specifically uses Coinbase — it's the most
    // recognizable consumer-facing brand on the rank list and reads cleanly
    // as "Connect Coinbase" in the preview chip.
    const i = sample.find(s => s.name === 'Coinbase') || sample[0];
    return (
      <div className="preview-btn preview-btn-single" style={{background: i?.primary || '#0F172A'}}>
        <img src={pickIcon(i, 'brand')} alt="" className="preview-icon"/>
        <span style={{color: i?.text || '#fff'}}>Connect {i?.name || 'Coinbase'}</span>
      </div>
    );
  }
  if (type === 'multi') {
    return (
      <div className="preview-btn preview-btn-multi">
        <div className="preview-cluster">
          {sample.slice(0, 4).map((i, idx) => (
            <div key={i.id} className="preview-cluster-icon" style={{zIndex: 4 - idx}}>
              <img src={pickIcon(i, 'light')} alt=""/>
            </div>
          ))}
        </div>
        <span>Connect account</span>
      </div>
    );
  }
  return (
    <div className="preview-btn preview-btn-cat">
      <div className="preview-grid-mini">
        {sample.slice(0, 6).map(i => (
          <div key={i.id} className="preview-grid-icon">
            <img src={pickIcon(i, 'light')} alt=""/>
          </div>
        ))}
      </div>
      <span>Connect a wallet</span>
    </div>
  );
}

// ---------- Integration grid ----------
function IntegrationCard({ integration, selected, onToggle }) {
  // Always use the icon-only mark in Step 2 — keeps Easy / Smart / Category
  // pickers visually consistent and lets the grid stay scannable at a glance.
  const iconSrc = pickIcon(integration);
  return (
    <button
      className={cx('int-card', 'int-card-iconmark', selected && 'int-card-selected', !iconSrc && 'int-card-empty')}
      onClick={onToggle}
      title={integration.name}
    >
      <div className="int-card-logo">
        {iconSrc
          ? <img src={iconSrc} alt={integration.name} loading="lazy"/>
          : <div className="int-card-fallback">{integration.name.slice(0, 2).toUpperCase()}</div>}
      </div>
      <div className="int-card-name">{integration.name}</div>
      {selected && (
        <div className="int-card-check">
          <Icon.Check size={11}/>
        </div>
      )}
    </button>
  );
}

// Curated featured set for the multi-select picker — names match the
// integrations.js `name` field exactly. uniqByName collapses platform
// duplicates (e.g. two "Coinbase" rows) before this set filters.
const FEATURED_INTEGRATION_NAMES = [
  'Binance', 'Coinbase', 'MetaMask', 'Trust Wallet', 'Phantom', 'Ledger',
];

function IntegrationCatalog({ integrations, selected, onToggle, mode, maxSelect }) {
  const [query, setQuery] = useState('');
  const [filter, setFilter] = useState('all');
  const [showAll, setShowAll] = useState(false);

  // Full filtered set (everything matching search + type filter).
  const filtered = useMemo(() => {
    let list = uniqByName(integrations).filter(i =>
      i.logoColor || i.logoLight || i.logoDark || i.iconColor || i.iconLight || i.iconDark
    );
    if (filter !== 'all') list = list.filter(i => i.type === filter || i.category === filter);
    if (query) {
      const q = query.toLowerCase();
      list = list.filter(i => i.name.toLowerCase().includes(q));
    }
    list.sort(byRank);
    return list;
  }, [integrations, query, filter]);

  // The featured shortlist is a curated subset of `filtered` — same filter +
  // search rules apply, but only items whose name appears in
  // FEATURED_INTEGRATION_NAMES are kept, ordered by that array.
  const featured = useMemo(() => {
    const featuredSet = new Set(FEATURED_INTEGRATION_NAMES);
    const byName = new Map(filtered.map(i => [i.name, i]));
    return FEATURED_INTEGRATION_NAMES
      .map(n => byName.get(n))
      .filter(Boolean);
  }, [filtered]);

  // Show the curated list when the user hasn't searched/filtered/expanded.
  // Show curation in any category filter (All / Exchanges / Wallets) — only
  // searching or clicking "See all" disables it. The featured list is itself
  // re-filtered by category, so toggling Exchanges shows the featured
  // exchanges only, plus a "See all N integrations" pill if more match.
  const isCurated = !showAll && !query;
  const visible = isCurated ? featured : filtered;
  const hiddenCount = filtered.length - featured.length;

  return (
    <div className="catalog">
      <div className="catalog-controls">
        <div className="search-wrap">
          <Icon.Search size={14}/>
          <input
            className="search-input"
            placeholder="Search 138 integrations…"
            value={query}
            onChange={e => setQuery(e.target.value)}
          />
          {query && <button className="search-clear" onClick={() => setQuery('')}><Icon.X size={12}/></button>}
        </div>
        <div className="filter-pills">
          {[
            ['all', 'All'],
            ['exchange', 'Exchanges'],
            ['deFiWallet', 'Wallets'],
          ].map(([v, label]) => (
            <button
              key={v}
              className={cx('pill', filter === v && 'pill-active')}
              onClick={() => setFilter(v)}
            >
              {label}
            </button>
          ))}
        </div>
        {(mode === 'multi' || mode === 'single') && (
          <div className="catalog-counter">
            <span className="counter-num">{selected.length}</span>
            <span className="counter-label">selected</span>
            {maxSelect && <span className="counter-max">/ {maxSelect}</span>}
          </div>
        )}
      </div>
      <div className="catalog-grid">
        {visible.map(i => (
          <IntegrationCard
            key={i.id}
            integration={i}
            selected={selected.some(s => s.id === i.id)}
            onToggle={() => onToggle(i)}
            mode={mode}
          />
        ))}
      </div>
      {isCurated && hiddenCount > 0 && (
        <div className="catalog-see-all-wrap">
          <button className="catalog-see-all" onClick={() => setShowAll(true)}>
            See all {filtered.length} integrations
            <Icon.ChevronRight size={14}/>
          </button>
        </div>
      )}
      {!isCurated && !query && (
        <div className="catalog-see-all-wrap">
          <button className="catalog-see-all catalog-see-all-collapse" onClick={() => setShowAll(false)}>
            Show featured only
          </button>
        </div>
      )}
      {filtered.length === 0 && (
        <div className="catalog-empty">
          No integrations match "{query}"
        </div>
      )}
    </div>
  );
}

// ---------- Button styling helper ----------
// SHARED between the live ButtonPreview and the embed-code generator so the
// emitted HTML is byte-for-byte the same look as the preview. If you change
// styling here, both update together.
const BTN_SIZE_MAP = {
  sm: { pad: '8px 14px', font: 13, icon: 16 },
  md: { pad: '11px 18px', font: 14, icon: 18 },
  lg: { pad: '14px 22px', font: 15, icon: 20 },
};
const BTN_RADIUS_MAP = { pill: 999, rounded: 10, square: 4 };

function computeButtonStyles({ mode, integration, config }) {
  const { theme, size, shape, style, customRadius, customBg, customFg, borderWidth, borderColor } = config;
  const isDark = theme === 'dark';
  const i = integration;

  let bg = '#0F172A';
  let fg = '#FFFFFF';
  let border = 'transparent';
  let shadow = 'none';

  if (mode === 'single' && i) {
    if (style === 'brand') {
      bg = isDark ? (i.primaryDark || i.primary || '#0F172A') : (i.primary || '#0F172A');
      fg = isDark ? (i.textDark || i.text || '#fff') : (i.text || '#fff');
      shadow = '0 1px 2px rgba(15,23,42,0.08)';
    } else if (style === 'outline') {
      bg = 'transparent';
      fg = isDark ? '#F1F5F9' : '#0F172A';
      border = isDark ? '#475569' : '#CBD5E1';
    } else if (style === 'light') {
      bg = isDark ? '#1E293B' : '#FFFFFF';
      fg = isDark ? '#F1F5F9' : '#0F172A';
      border = isDark ? '#1E293B' : '#FFFFFF';
      shadow = isDark
        ? '0 1px 2px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.06)'
        : '0 1px 2px rgba(15,23,42,0.06), 0 4px 12px rgba(15,23,42,0.06)';
    } else {
      bg = isDark ? '#F8FAFC' : '#0F172A';
      fg = isDark ? '#0F172A' : '#FFFFFF';
      shadow = '0 1px 2px rgba(15,23,42,0.08)';
    }
  } else {
    if (style === 'outline') {
      bg = 'transparent';
      fg = isDark ? '#F1F5F9' : '#0F172A';
      border = isDark ? '#475569' : '#CBD5E1';
    } else if (style === 'light') {
      bg = isDark ? '#1E293B' : '#FFFFFF';
      fg = isDark ? '#F1F5F9' : '#0F172A';
      border = isDark ? '#1E293B' : '#FFFFFF';
      shadow = isDark
        ? '0 1px 2px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.06)'
        : '0 1px 2px rgba(15,23,42,0.06), 0 4px 12px rgba(15,23,42,0.06)';
    } else {
      bg = isDark ? '#F8FAFC' : '#0F172A';
      fg = isDark ? '#0F172A' : '#FFFFFF';
      shadow = '0 1px 2px rgba(15,23,42,0.08)';
    }
  }

  // Apply user overrides on top of preset-derived values.
  if (customBg)    bg = customBg;
  if (customFg)    fg = customFg;
  if (borderColor) border = borderColor;

  const sz = BTN_SIZE_MAP[size] || BTN_SIZE_MAP.md;
  const presetRadius = BTN_RADIUS_MAP[shape] || 10;
  const radius = (customRadius != null && customRadius >= 0) ? customRadius : presetRadius;
  const bw = (borderWidth != null && borderWidth >= 0) ? borderWidth : 1;
  const borderCss = bw === 0 ? 'none' : `${bw}px solid ${border}`;
  const iconSurface = (mode === 'single' && style === 'brand') ? 'brand' : (isDark ? 'dark' : 'light');

  return { bg, fg, border, borderCss, shadow, sz, radius, iconSurface, isDark };
}

// ---------- Button preview ----------
// Renders a single button. For single-mode with multiple integrations,
// the parent renders a stack of these (one per integration).
function ButtonPreview({ mode, selected, integration, config, onLaunch }) {
  const { label, style, showIcon } = config;
  
  // Compute subtext - use default based on mode
  const subtext = defaultDepositSubtext({ mode });

  // In single mode, the per-button integration is whichever one this button
  // represents. In multi/category, fall back to the cluster.
  const i = integration || (mode === 'single' ? selected[0] : null);
  const { bg, fg, borderCss, shadow, sz, radius, iconSurface, isDark } =
    computeButtonStyles({ mode, integration: i, config });

  const btnStyle = {
    background: bg,
    color: fg,
    border: borderCss,
    padding: sz.pad,
    fontSize: sz.font,
    borderRadius: radius,
    boxShadow: shadow,
  };

  let displayLabel = label;
  if (displayLabel && mode === 'single' && i) {
    // Allow {provider} template substitution so a single label string can
    // resolve to the right provider name on each standalone button.
    displayLabel = displayLabel.replace(/\{provider\}/g, i.name);
  } else if (displayLabel && /\{provider\}/.test(displayLabel)) {
    // Aggregator/category buttons can't resolve {provider} — use the default.
    displayLabel = defaultDepositLabel({ mode, integration: i, category: config.category });
  }
  if (!displayLabel) {
    displayLabel = defaultDepositLabel({ mode, integration: i, category: config.category });
  }

  return (
    <button className="rendered-btn" style={btnStyle} onClick={onLaunch}>
      {showIcon && (
        <span className="rendered-btn-icon">
          {mode === 'single' && i ? (
            <img src={pickIcon(i, iconSurface)} alt="" style={{height: sz.icon}}/>
          ) : (mode === 'multi' || mode === 'category') && selected.length > 0 ? (
            <span className="cluster-icons">
              {selected.slice(0, 4).map((it, idx) => (
                <span key={it.id} className="cluster-icon" style={{zIndex: 5 - idx, height: sz.icon, width: sz.icon}}>
                  <img src={pickIcon(it, isDark ? 'dark' : 'light')} alt=""/>
                </span>
              ))}
              {selected.length > 4 && (
                <span className="cluster-icon cluster-more" style={{height: sz.icon, minWidth: sz.icon, padding: '0 6px', fontSize: sz.icon * 0.55}}>
                  +{selected.length - 4}
                </span>
              )}
            </span>
          ) : (
            <Icon.Mesh size={sz.icon}/>
          )}
        </span>
      )}
      <span className="rendered-btn-text">
        <span className="rendered-btn-label">{displayLabel}</span>
        {subtext && <span className="rendered-btn-subtext">{subtext}</span>}
      </span>
    </button>
  );
}

// Renders the right number of buttons for the current mode.
// Single + multiple selected => one button per integration (stacked).
// Single + 0 selected         => placeholder button.
// Multi / category            => one aggregator button.
function ButtonPreviewArea({ mode, selected, config, onLaunch }) {
  if (mode === 'single' && selected.length > 1) {
    return (
      <div className="btn-stack">
        {selected.map(i => (
          <ButtonPreview
            key={i.id}
            mode={mode}
            selected={selected}
            integration={i}
            config={config}
            onLaunch={() => onLaunch(i)}
          />
        ))}
      </div>
    );
  }
  return (
    <ButtonPreview
      mode={mode}
      selected={selected}
      integration={mode === 'single' ? selected[0] : null}
      config={config}
      onLaunch={() => onLaunch(selected[0])}
    />
  );
}

// Renders ALL of a user's routes stacked together — the live preview of what
// will end up on their checkout page. Each route's Launch handler reports its
// route + (optionally) the integration that was clicked, so the modal can open
// in the right state.
function RouteStack({ routes, config, resolveRouteProviders, onLaunch }) {
  if (!routes.length) return null;
  return (
    <div className="route-stack">
      {routes.map(route => {
        const providers = resolveRouteProviders(route);
        // Per-route label overrides the (now legacy) shared config.label.
        const scopedConfig = { ...config, label: route.label || '' };
        return (
          <div key={route.id} className="route-stack-item">
            <ButtonPreviewArea
              mode={route.kind}
              selected={providers}
              config={scopedConfig}
              onLaunch={(integration) => onLaunch({ route, integration: route.kind === 'single' ? integration : null })}
            />
          </div>
        );
      })}
    </div>
  );
}

// ---------- Mock app context ----------
function MockAppContext({ children, dark }) {
  return (
    <div className={cx('mock-app', dark && 'mock-app-dark')}>
      <div className="mock-app-chrome">
        <div className="mock-app-dots">
          <span/><span/><span/>
        </div>
        <div className="mock-app-url">app.yourcompany.com</div>
      </div>
      <div className="mock-app-body">
        <div className="mock-app-header">
          <div className="mock-logo">
            <div className="mock-logo-mark"/>
            <span>Acme Finance</span>
          </div>
          <div className="mock-app-nav">
            <span>Portfolio</span>
            <span className="active">Connect</span>
            <span>Settings</span>
          </div>
        </div>
        <div className="mock-app-content">
          <div className="mock-card">
            <div className="mock-eyebrow">Add an account</div>
            <h2>Bring your assets in</h2>
            <p>Connect securely with bank-level encryption. Your credentials never touch our servers.</p>
            <div className="mock-button-slot">
              {children}
            </div>
            <div className="mock-tos">
              By connecting, you agree to our <span>Terms</span> and <span>Privacy Policy</span>.
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

// ---------- Realistic surfaces (Step 3 preview) ----------
//
// These mockup surfaces let evaluators see the deposit button in context — not
// just floating in a card. Each surface is intentionally generic (no Mesh
// branding, plausible labels) so it reads as "any product surface" rather than
// "a Mesh-branded screen."
//
//   - SurfaceExchange: trading dashboard (browser chrome, desktop crypto vibe)
//   - SurfaceFintech:  banking/personal-finance app (iPhone frame, mobile)
//   - SurfaceCheckout: e-commerce checkout drawer (browser chrome)
//
// All surfaces accept { dark, children } and slot the live RouteStack into a
// natural place — wherever a real product would put the "Add funds" CTA.

function SurfaceExchange({ children, dark }) {
  // Generic crypto/trading dashboard. Side nav, balance card, deposit CTA.
  // Mostly chrome and dummy UI; the deposit button slot is the visual focus.
  return (
    <div className={cx('surface-exchange', dark && 'surface-exchange-dark')}>
      <div className="surface-browser">
        <div className="surface-browser-bar">
          <div className="surface-browser-dots"><span/><span/><span/></div>
          <div className="surface-browser-url">
            <span className="surface-browser-url-lock">🔒</span>
            <span>app.exchange.com / dashboard</span>
          </div>
          <div className="surface-browser-spacer"/>
        </div>
        <div className="surface-exchange-body">
          <aside className="surface-exchange-nav">
            <div className="surface-exchange-logo"/>
            <div className="surface-exchange-nav-items">
              <span>Dashboard</span>
              <span className="active">Wallet</span>
              <span>Markets</span>
              <span>Trade</span>
              <span>History</span>
            </div>
          </aside>
          <main className="surface-exchange-main">
            <div className="surface-exchange-crumbs">Wallet · Balances</div>
            <div className="surface-exchange-balance-card">
              <div className="surface-exchange-balance-label">Total balance</div>
              <div className="surface-exchange-balance-value">$0.00</div>
              <div className="surface-exchange-balance-sub">Fund your wallet to start trading</div>
              <div className="surface-exchange-cta-slot">{children}</div>
            </div>
            <div className="surface-exchange-grid">
              <div className="surface-exchange-tile">
                <div className="surface-exchange-tile-head">Top movers</div>
                <div className="surface-exchange-rows">
                  <div className="surface-exchange-row"><span>BTC</span><span className="up">+2.4%</span></div>
                  <div className="surface-exchange-row"><span>ETH</span><span className="up">+1.8%</span></div>
                  <div className="surface-exchange-row"><span>SOL</span><span className="down">−0.6%</span></div>
                </div>
              </div>
              <div className="surface-exchange-tile">
                <div className="surface-exchange-tile-head">Watchlist</div>
                <div className="surface-exchange-empty">Add tokens to follow</div>
              </div>
            </div>
          </main>
        </div>
      </div>
    </div>
  );
}

function SurfaceFintech({ children, dark }) {
  // iPhone-style portrait frame, fintech "Add funds" screen. The button slot
  // sits under a balance card so evaluators see it as the primary CTA.
  return (
    <div className="surface-fintech-wrap">
      <div className={cx('surface-iphone', dark && 'surface-iphone-dark')}>
        <div className="surface-iphone-notch"/>
        <div className="surface-iphone-screen">
          <div className="surface-iphone-status">
            <span>9:41</span>
            <span className="surface-iphone-status-icons">
              <span>●●●●</span>
              <span>5G</span>
              <span>100</span>
            </span>
          </div>
          <div className="surface-fintech-body">
            <div className="surface-fintech-header">
              <div className="surface-fintech-back">‹</div>
              <div className="surface-fintech-title">Add funds</div>
              <div className="surface-fintech-help">?</div>
            </div>
            <div className="surface-fintech-balance">
              <div className="surface-fintech-balance-label">Available balance</div>
              <div className="surface-fintech-balance-value">$0.00</div>
            </div>
            <div className="surface-fintech-section">
              <div className="surface-fintech-section-label">Deposit method</div>
              <div className="surface-fintech-cta-slot">{children}</div>
              <div className="surface-fintech-foot">
                <span>🔒</span>
                <span>Bank-level encryption</span>
              </div>
            </div>
          </div>
        </div>
        <div className="surface-iphone-home"/>
      </div>
    </div>
  );
}

function SurfaceCheckout({ children, dark }) {
  // E-commerce / SaaS-style checkout panel. Order summary on left,
  // payment-method panel on right where the deposit/connect CTA lives.
  return (
    <div className={cx('surface-checkout', dark && 'surface-checkout-dark')}>
      <div className="surface-browser">
        <div className="surface-browser-bar">
          <div className="surface-browser-dots"><span/><span/><span/></div>
          <div className="surface-browser-url">
            <span className="surface-browser-url-lock">🔒</span>
            <span>checkout.yourbrand.com</span>
          </div>
          <div className="surface-browser-spacer"/>
        </div>
        <div className="surface-checkout-body">
          <div className="surface-checkout-summary">
            <div className="surface-checkout-eyebrow">Order summary</div>
            <div className="surface-checkout-line">
              <div className="surface-checkout-thumb"/>
              <div className="surface-checkout-line-info">
                <div className="surface-checkout-line-name">Pro plan · Annual</div>
                <div className="surface-checkout-line-meta">Billed yearly</div>
              </div>
              <div className="surface-checkout-line-price">$240.00</div>
            </div>
            <div className="surface-checkout-divider"/>
            <div className="surface-checkout-totals">
              <div className="surface-checkout-row"><span>Subtotal</span><span>$240.00</span></div>
              <div className="surface-checkout-row"><span>Tax</span><span>$0.00</span></div>
              <div className="surface-checkout-row surface-checkout-total"><span>Total</span><span>$240.00</span></div>
            </div>
          </div>
          <div className="surface-checkout-pay">
            <div className="surface-checkout-eyebrow">Pay with crypto</div>
            <div className="surface-checkout-pay-desc">Connect your wallet or exchange to pay $240.00 in crypto.</div>
            <div className="surface-checkout-cta-slot">{children}</div>
            <div className="surface-checkout-orsep">— or —</div>
            <button className="surface-checkout-altpay" disabled>Pay with card</button>
          </div>
        </div>
      </div>
    </div>
  );
}

// ---------- Mesh Link mock modal ----------
function MeshLinkModal({ open, integrations, mode, onClose }) {
  const [stage, setStage] = useState('list');
  useEffect(() => {
    if (open) {
      setStage('list');
      const t = setTimeout(() => setStage('connecting'), 1800);
      const t2 = setTimeout(() => setStage('done'), 3400);
      const t3 = setTimeout(() => onClose(), 4800);
      return () => { clearTimeout(t); clearTimeout(t2); clearTimeout(t3); };
    }
  }, [open]);

  if (!open) return null;
  const list = uniqByName(integrations).slice(0, 8);

  return (
    <div className="mesh-modal-backdrop" onClick={onClose}>
      <div className="mesh-modal" onClick={e => e.stopPropagation()}>
        <div className="mesh-modal-header">
          <div className="mesh-modal-brand">
            <img src="assets/mesh-logo.png" alt="Mesh" className="mesh-modal-brand-logo"/>
          </div>
          <button className="mesh-modal-close" onClick={onClose}><Icon.X size={14}/></button>
        </div>
        {stage === 'list' && (
          <>
            <div className="mesh-modal-title">
              {mode === 'single' ? `Connecting to ${integrations[0]?.name}…` :
               mode === 'category' ? 'Choose your provider' : 'Choose your account'}
            </div>
            <div className="mesh-modal-list">
              {list.map(i => (
                <div key={i.id} className="mesh-modal-item">
                  <div className="mesh-modal-item-icon">
                    <img src={pickIcon(i, 'light')} alt=""/>
                  </div>
                  <div className="mesh-modal-item-name">{i.name}</div>
                  <Icon.ChevronRight size={14}/>
                </div>
              ))}
            </div>
          </>
        )}
        {stage === 'connecting' && (
          <div className="mesh-modal-state">
            <div className="mesh-spinner"/>
            <div className="mesh-state-title">Authenticating securely</div>
            <div className="mesh-state-sub">This won't take long</div>
          </div>
        )}
        {stage === 'done' && (
          <div className="mesh-modal-state">
            <div className="mesh-success"><Icon.Check size={28}/></div>
            <div className="mesh-state-title">You're connected</div>
            <div className="mesh-state-sub">Returning you to the app</div>
          </div>
        )}
        <div className="mesh-modal-footer">
          <span>Secured by</span>
          <img src="assets/mesh-logo.png" alt="Mesh" className="mesh-modal-foot-logo"/>
        </div>
      </div>
    </div>
  );
}

// ---------- Embed code generator ----------
// Emits a real <button> with full inline styling that matches the preview
// exactly (background, padding, radius, shadow, icon, label) plus a click
// handler that opens the Mesh Link modal. The styling lives in your HTML —
// the SDK only owns the connection flow. This is how Stripe Elements / Plaid
// Link / etc. ship: hand the developer a button they own, modal stays ours.

const slugify = (s) =>
  s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');

// Stable, unique id for a config — same selection => same DOM id, so it's
// safe to drop the snippet in twice.
const embedIdFor = ({ mode, selected, config }) => {
  if (mode === 'single' && selected[0]) return `mesh-btn-${slugify(selected[0].name)}`;
  if (mode === 'category') return `mesh-btn-${config.category}`;
  if (mode === 'multi') return 'mesh-btn-multi';
  return 'mesh-btn';
};

const styleObjToCss = (obj) =>
  Object.entries(obj)
    .filter(([, v]) => v !== undefined && v !== null && v !== '')
    .map(([k, v]) => {
      const prop = k.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
      const val = typeof v === 'number' && !/^(z-?index|line-height|opacity|font-weight)$/.test(prop) ? `${v}px` : v;
      return `${prop}: ${val}`;
    })
    .join('; ');

// Build the button-face inline HTML (icon + label) for embed code.
function buildEmbedButtonInner({ mode, selected, integration, config }) {
  const { showIcon, label } = config;
  const { sz, iconSurface, isDark } = computeButtonStyles({ mode, integration, config });

  let displayLabel = label;
  if (displayLabel && mode === 'single' && integration) {
    displayLabel = displayLabel.replace(/\{provider\}/g, integration.name);
  } else if (displayLabel && /\{provider\}/.test(displayLabel)) {
    displayLabel = defaultDepositLabel({ mode, integration, category: config.category });
  }
  if (!displayLabel) {
    displayLabel = defaultDepositLabel({ mode, integration, category: config.category });
  }

  const escapeAttr = (s) => String(s).replace(/"/g, '&quot;').replace(/</g, '&lt;');

  let iconHtml = '';
  if (showIcon) {
    if (mode === 'single' && integration) {
      const src = pickIcon(integration, iconSurface);
      iconHtml = `<img src="${src}" alt="" style="height: ${sz.icon}px; width: auto; display: block;"/>`;
    } else if ((mode === 'multi' || mode === 'category') && selected.length > 0) {
      const surface = isDark ? 'dark' : 'light';
      const cluster = selected.slice(0, 4).map((it, idx) => {
        const src = pickIcon(it, surface);
        const wrapStyle = styleObjToCss({
          display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
          height: sz.icon, width: sz.icon,
          borderRadius: '50%',
          background: isDark ? '#1E293B' : '#FFFFFF',
          boxShadow: isDark ? '0 0 0 2px #1E293B' : '0 0 0 2px #FFFFFF',
          marginLeft: idx === 0 ? 0 : -6,
          zIndex: 5 - idx,
          overflow: 'hidden',
        });
        return `<span style="${wrapStyle}"><img src="${src}" alt="" style="height: ${Math.round(sz.icon * 0.78)}px; width: ${Math.round(sz.icon * 0.78)}px; object-fit: contain;"/></span>`;
      }).join('');
      let extra = '';
      if (selected.length > 4) {
        const moreStyle = styleObjToCss({
          display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
          height: sz.icon, minWidth: sz.icon,
          padding: '0 6px',
          fontSize: Math.round(sz.icon * 0.55),
          fontWeight: 600,
          borderRadius: 999,
          background: isDark ? '#334155' : '#E2E8F0',
          color: isDark ? '#F1F5F9' : '#0F172A',
          marginLeft: -6,
          zIndex: 1,
        });
        extra = `<span style="${moreStyle}">+${selected.length - 4}</span>`;
      }
      const wrapStyle = styleObjToCss({ display: 'inline-flex', alignItems: 'center' });
      iconHtml = `<span style="${wrapStyle}">${cluster}${extra}</span>`;
    } else {
      // Mesh fallback mark
      iconHtml = `<svg width="${sz.icon}" height="${sz.icon}" viewBox="0 0 32 32" fill="none" aria-hidden="true"><rect width="32" height="32" rx="8" fill="#0F172A"/><path d="M8 22V10l4 4 4-4 4 4 4-4v12" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
    }
  }

  return `${iconHtml}<span>${escapeAttr(displayLabel)}</span>`;
}

function buildEmbedButtonStyle({ mode, integration, config }) {
  const { bg, fg, borderCss, shadow, sz, radius } = computeButtonStyles({ mode, integration, config });
  return styleObjToCss({
    display: 'inline-flex',
    alignItems: 'center',
    gap: 10,
    background: bg,
    color: fg,
    border: borderCss,
    padding: sz.pad,
    fontSize: sz.font,
    fontWeight: 600,
    fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
    borderRadius: radius,
    boxShadow: shadow,
    cursor: 'pointer',
    lineHeight: 1.2,
  });
}

function buildEmbedButtonStyleObject({ mode, integration, config }) {
  const { bg, fg, borderCss, shadow, sz, radius } = computeButtonStyles({ mode, integration, config });
  return {
    display: 'inline-flex',
    alignItems: 'center',
    gap: 10,
    background: bg,
    color: fg,
    border: borderCss,
    padding: sz.pad,
    fontSize: sz.font,
    fontWeight: 600,
    fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
    borderRadius: radius,
    boxShadow: shadow,
    cursor: 'pointer',
    lineHeight: 1.2,
  };
}

// Generate the data-mesh-button attribute value for a button.
function getMeshButtonKey(mode, integration, route) {
  if (mode === 'single' && integration) return slugify(integration.name);
  if (mode === 'category') return route?.category || 'catalog';
  return 'catalog';
}

function generateEmbed({ routes, config, resolveRouteProviders }) {
  // Renders one button element.
  const renderButton = (mode, integration, selected, blockConfig = config, route = null) => {
    const styleAttr = buildEmbedButtonStyle({ mode, integration, config: blockConfig });
    const inner = buildEmbedButtonInner({ mode, selected, integration, config: blockConfig });

    return `<button class="mesh-btn" style="${styleAttr}">
  ${inner}
</button>`;
  };

  if (!routes || !routes.length) {
    return `<!-- Pick at least one route to generate code -->`;
  }

  // Collect all buttons
  const buttons = [];

  routes.forEach((route) => {
    const providers = resolveRouteProviders(route);
    const scopedConfig = { ...config, label: route.label || '' };

    if (route.kind === 'single') {
      providers.forEach((p) => {
        buttons.push({ html: renderButton('single', p, [p], scopedConfig, route) });
      });
    } else {
      buttons.push({ html: renderButton(route.kind, null, providers, scopedConfig, route) });
    }
  });

  const buttonHtml = buttons.map((b) => b.html).join("\n\n")

  return `<!-- Mesh Connect Button Styling -->
<!-- For SDK integration, see: https://docs.meshconnect.com/guides/launch-link -->

<style>
  .mesh-btn {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    cursor: pointer;
    line-height: 1.2;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  }
  .mesh-btn:hover {
    opacity: 0.9;
  }
</style>

${buttonHtml}`;
}

// Convert a JS style object to source code representation
function jsStyleObjectToSource(obj, indent = 4) {
  const pad = ' '.repeat(indent);
  const lines = Object.entries(obj).map(([k, v]) => {
    const val = typeof v === 'number' ? String(v) : JSON.stringify(v);
    return pad + k + ': ' + val + ',';
  });
  return '{' + '\n' + lines.join('\n') + '\n' + ' '.repeat(indent - 2) + '}';
}

function generateReactEmbed({ routes, config, resolveRouteProviders }) {
  if (!routes || !routes.length) return '// Pick at least one route to generate code.';

  // Collect buttons
  const buttons = [];

  routes.forEach((route) => {
    const providers = resolveRouteProviders(route);
    if (route.kind === 'single') {
      providers.forEach((p) => {
        const styleObj = buildEmbedButtonStyleObject({ mode: 'single', integration: p, config });
        const inner = config.showIcon
          ? `<img src="${p.iconUrl}" alt="" style={{ height: 20, width: 'auto', borderRadius: 4 }} />`
          : '';
        const routeLabel = route.label || '';
        const labelText = routeLabel
          ? routeLabel.replace(/{provider}/g, p.name)
          : defaultDepositLabel({ mode: 'single', integration: p });
        buttons.push({ styleObj, inner, labelText });
      });
    } else {
      const styleObj = buildEmbedButtonStyleObject({ mode: route.kind, integration: null, config });
      const labelText = (route.label && !/{provider}/.test(route.label)) ? route.label : defaultDepositLabel({ mode: route.kind, category: route.category });
      buttons.push({ styleObj, inner: '', labelText });
    }
  });

  const buttonJsx = buttons.map((b) =>
`      <button
        style={${jsStyleObjectToSource(b.styleObj, 10)}}
        onClick={handleMeshClick}
      >
        ${b.inner ? b.inner + ' ' : ''}<span>${b.labelText}</span>
      </button>`).join("\n\n")

  return `// Mesh Connect Button Styling
// For SDK integration, see: https://docs.meshconnect.com/guides/launch-link

export function MeshButtons() {
  const handleMeshClick = () => {
    // Implement your Mesh SDK integration here
    // See docs: https://docs.meshconnect.com/guides/launch-link
  };

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
${buttonJsx}
    </div>
  );
}`;
}

function generateVueEmbed({ routes, config, resolveRouteProviders }) {
  if (!routes || !routes.length) return '<!-- Pick at least one route to generate code. -->';

  const buttons = [];

  routes.forEach((route) => {
    const providers = resolveRouteProviders(route);
    if (route.kind === 'single') {
      providers.forEach((p) => {
        const styleObj = buildEmbedButtonStyleObject({ mode: 'single', integration: p, config });
        const inner = config.showIcon
          ? `<img src="${p.iconUrl}" alt="" :style="{ height: '20px', width: 'auto', display: 'block' }"/>`
          : '';
        const routeLabel = route.label || '';
        const labelText = routeLabel
          ? routeLabel.replace(/{provider}/g, p.name)
          : defaultDepositLabel({ mode: 'single', integration: p });
        buttons.push({ styleObj, inner, labelText });
      });
    } else {
      const styleObj = buildEmbedButtonStyleObject({ mode: route.kind, integration: null, config });
      const labelText = (route.label && !/{provider}/.test(route.label)) ? route.label : defaultDepositLabel({ mode: route.kind, category: route.category });
      buttons.push({ styleObj, inner: '', labelText });
    }
  });

  const buttonsTemplate = buttons.map((b, i) =>
`    <button :style="btnStyle${i}" @click="handleMeshClick">
      ${b.inner ? b.inner + ' ' : ''}<span>${b.labelText}</span>
    </button>`).join("\n")

  const styleData = buttons.map((b, i) => `      btnStyle${i}: ${jsStyleObjectToSource(b.styleObj, 8)},`).join("\n")

  return `<!-- Mesh Connect Button Styling -->
<!-- For SDK integration, see: https://docs.meshconnect.com/guides/launch-link -->

<template>
  <div :style="{ display: 'flex', flexDirection: 'column', gap: '12px' }">
${buttonsTemplate}
  </div>
</template>

<script>
export default {
  data() {
    return {
${styleData}
    };
  },
  methods: {
    handleMeshClick() {
      // Implement your Mesh SDK integration here
      // See docs: https://docs.meshconnect.com/guides/launch-link
    },
  },
};
</script>`;
}

function CodeBlock({ snippets }) {
  // snippets: [{ id, label, code }]
  const [activeId, setActiveId] = useState(snippets[0]?.id);
  const [copied, setCopied] = useState(false);
  const active = snippets.find((s) => s.id === activeId) || snippets[0];
  const code = active?.code || '';
  const copy = () => {
    navigator.clipboard?.writeText(code);
    setCopied(true);
    setTimeout(() => setCopied(false), 1600);
  };

  // Token-based syntax highlighting (single pass, no overlapping replacement)
  const escapeHtml = (s) => s.replace(/[&<>]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' })[c]);
  const tokenize = (src) => {
    const tokens = [];
    let i = 0;
    const KW = /^(const|var|let|function|return|new|if|else)\b/;
    while (i < src.length) {
      const rest = src.slice(i);
      // HTML comment
      const hc = rest.match(/^<!--[\s\S]*?-->/);
      if (hc) { tokens.push(['com', hc[0]]); i += hc[0].length; continue; }
      // JS comment
      const jc = rest.match(/^\/\/[^\n]*/);
      if (jc) { tokens.push(['com', jc[0]]); i += jc[0].length; continue; }
      // String
      const str = rest.match(/^('[^'\n]*'|"[^"\n]*")/);
      if (str) { tokens.push(['str', str[0]]); i += str[0].length; continue; }
      // HTML tag (open or close)
      const tag = rest.match(/^<\/?[a-zA-Z][\w-]*/);
      if (tag) { tokens.push(['tag', tag[0]]); i += tag[0].length; continue; }
      const tagEnd = rest.match(/^\/?>/);
      if (tagEnd) { tokens.push(['tag', tagEnd[0]]); i += tagEnd[0].length; continue; }
      // Keyword
      const kw = rest.match(KW);
      if (kw) { tokens.push(['kw', kw[0]]); i += kw[0].length; continue; }
      // Object key
      const key = rest.match(/^([a-zA-Z_]\w*)(\s*:)/);
      if (key) { tokens.push(['key', key[1]]); tokens.push(['plain', key[2]]); i += key[0].length; continue; }
      // Whitespace
      const ws = rest.match(/^\s+/);
      if (ws) { tokens.push(['plain', ws[0]]); i += ws[0].length; continue; }
      // Word
      const word = rest.match(/^\w+/);
      if (word) { tokens.push(['plain', word[0]]); i += word[0].length; continue; }
      // Single char
      tokens.push(['plain', src[i]]);
      i++;
    }
    return tokens;
  };
  const highlighted = tokenize(code).map(([type, val]) => {
    const safe = escapeHtml(val);
    return type === 'plain' ? safe : `<span class="tk-${type}">${safe}</span>`;
  }).join('');

  return (
    <div className="code-block">
      <div className="code-header">
        <div className="code-tabs">
          {snippets.map((s) => (
            <button
              key={s.id}
              className={cx('code-tab', s.id === active?.id && 'code-tab-active')}
              onClick={() => setActiveId(s.id)}
            >{s.label}</button>
          ))}
        </div>
        <button className={cx('btn-copy', copied && 'btn-copy-done')} onClick={copy}>
          {copied ? <><Icon.Check size={12}/> Copied</> : <><Icon.Copy size={12}/> Copy</>}
        </button>
      </div>
      <pre className="code-body"><code dangerouslySetInnerHTML={{ __html: highlighted }}/></pre>
    </div>
  );
}

// ---------- Export modal ----------
function ExportModal({ open, snippets, onClose }) {
  useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', onKey);
    document.body.style.overflow = 'hidden';
    return () => {
      window.removeEventListener('keydown', onKey);
      document.body.style.overflow = '';
    };
  }, [open, onClose]);

  if (!open) return null;
  return (
    <div className="export-modal-overlay" onClick={onClose}>
      <div className="export-modal" onClick={(e) => e.stopPropagation()}>
        <div className="export-modal-header">
          <div>
            <div className="export-modal-eyebrow">Ready to ship</div>
            <h3>Export code</h3>
            <p>Drop the snippet for your stack into your app. The SDK only owns the modal — the buttons are yours.</p>
          </div>
          <button className="export-modal-close" onClick={onClose} aria-label="Close"><Icon.X size={16}/></button>
        </div>

        <div className="export-modal-body">
          <CodeBlock snippets={snippets}/>

          <div className="install-note">
            <div className="install-note-title">Next steps</div>
            <p style={{margin: '8px 0 0 0'}}>
              This code provides the button styling. To integrate with the Mesh SDK and launch the Link flow, see the <a href="https://docs.meshconnect.com/guides/launch-link" target="_blank" rel="noopener noreferrer" style={{color: '#7C3AED'}}>Mesh SDK documentation</a>.
            </p>
          </div>
        </div>
      </div>
    </div>
  );
}

// ---------- Customizer panel ----------

// Per-route Label section shown in Step 2. Each route owns its own label
// string. Chips are scoped to the route's lifecycle (Easy/Smart/Category) so
// the suggestions always make sense for the active context. {provider} is
// a literal token kept verbatim — substitution happens at render time for
// single-mode buttons.
function RouteLabelSection({ route, onChange }) {
  const firstSingleProvider = (() => {
    if (route.kind !== 'single') return null;
    return route.selected?.[0] || null;
  })();

  // Lifecycle-specific defaults + chip suggestions.
  const presets = (() => {
    if (route.kind === 'single') {
      return ['Deposit with {provider}', '{provider}', 'Quick deposit', 'Connect {provider}'];
    }
    if (route.kind === 'multi') {
      return ['Smart deposit', 'Secure deposit', 'Direct deposit', 'Connect your account'];
    }
    if (route.kind === 'category') {
      if (route.category === 'exchange') return ['Deposit from an exchange', 'Connect your exchange', 'Deposit'];
      if (route.category === 'deFiWallet') return ['Deposit from a wallet', 'Connect your wallet', 'Deposit'];
      return ['Deposit', 'Connect account'];
    }
    return [];
  })();

  const placeholder = (() => {
    if (route.kind === 'single') return firstSingleProvider ? `Deposit with ${firstSingleProvider.name}` : 'Deposit with {provider}';
    if (route.kind === 'multi') return 'Direct deposit';
    if (route.kind === 'category') {
      if (route.category === 'exchange') return 'Deposit from an exchange';
      if (route.category === 'deFiWallet') return 'Deposit from a wallet';
      return 'Deposit';
    }
    return 'Deposit';
  })();

  const labelHasTemplate = /\{provider\}/.test(route.label || '');

  return (
    <div className="route-label-section">
      <div className="route-label-head">
        <div className="route-label-title">Button label</div>
        <div className="route-label-sub">
          {route.kind === 'single' && 'Use {provider} to auto-fill each provider name across your standalone buttons.'}
          {route.kind === 'multi' && 'One label for the aggregator button.'}
          {route.kind === 'category' && 'One label for the category button.'}
        </div>
      </div>
      <div className="cust-presets">
        {presets.map(p => (
          <button
            key={p}
            type="button"
            className={cx('cust-preset', route.label === p && 'cust-preset-active')}
            onClick={() => onChange(p)}
          >{p}</button>
        ))}
        {route.label && (
          <button
            type="button"
            className="cust-preset cust-preset-clear"
            onClick={() => onChange('')}
            title="Clear and use the default"
          >Clear</button>
        )}
      </div>
      <input
        className="cust-text"
        placeholder={placeholder}
        value={route.label || ''}
        onChange={e => onChange(e.target.value)}
      />
      {labelHasTemplate && route.kind === 'single' && (
        <div className="cust-hint">Each button will substitute its own provider name in place of <code>{'{provider}'}</code>.</div>
      )}
    </div>
  );
}

// ---------- Customizer panel ----------
// Style presets — opinionated combos that feel like real-world shipped buttons.
// Picking one writes all five style keys (theme, style, size, shape, showIcon)
// at once. Each preset has a tiny visual signature so users can scan, not read.
const STYLE_PRESETS = [
  { id: 'default',  name: 'Default',  config: { theme: 'light', style: 'neutral', size: 'md', shape: 'rounded', showIcon: true } },
  { id: 'bold',     name: 'Bold',     config: { theme: 'light', style: 'brand',   size: 'lg', shape: 'rounded', showIcon: true } },
  { id: 'pill',     name: 'Pill',     config: { theme: 'light', style: 'light',   size: 'md', shape: 'pill',    showIcon: true } },
  { id: 'dark',     name: 'Dark',     config: { theme: 'dark',  style: 'neutral', size: 'md', shape: 'rounded', showIcon: true } },
];

function Customizer({ config, setConfig, routes = [], resolveRouteProviders, updateRoute }) {
  // Plain field setter.
  const set = (k, v) => setConfig(c => ({ ...c, [k]: v }));
  // Setter for the "structural" controls (theme/style/shape). Changing these
  // would otherwise be invisible if a custom override is locked in — so we
  // clear the relevant overrides as the user moves between presets. This
  // matches the user mental model: picking a Shape *means* you want that
  // shape's radius, not the stale custom one.
  const setStructural = (k, v) => setConfig(c => {
    const next = { ...c, [k]: v };
    if (k === 'shape') {
      next.customRadius = null;
    } else if (k === 'style') {
      next.customBg = null;
      next.customFg = null;
      next.borderColor = null;
      next.borderWidth = null;
    } else if (k === 'theme') {
      next.customBg = null;
      next.customFg = null;
      next.borderColor = null;
    }
    return next;
  });

  // Determine which preset matches current config (if any) so we can highlight it.
  // Custom overrides "break" the preset match — show no preset selected.
  const hasOverrides = config.customRadius != null || config.customBg || config.customFg
    || config.borderWidth != null || config.borderColor;
  const activePreset = hasOverrides ? null : STYLE_PRESETS.find(p =>
    Object.entries(p.config).every(([k, v]) => config[k] === v)
  );
  const applyPreset = (preset) => {
    // Picking a preset clears any custom overrides so the preset reads cleanly.
    setConfig(c => ({
      ...c,
      ...preset.config,
      customRadius: null,
      customBg: null,
      customFg: null,
      borderWidth: null,
      borderColor: null,
    }));
  };

  // Inspect the route mix to decide what UI options make sense.
  const hasSingle = routes.some(r => r.kind === 'single');
  const hasAggregator = routes.some(r => r.kind === 'multi' || r.kind === 'category');
  const hasCategory = routes.some(r => r.kind === 'category');
  const onlySingle = hasSingle && !hasAggregator;
  const firstSingleProvider = (() => {
    const r = routes.find(rr => rr.kind === 'single');
    return r && r.selected[0];
  })();

  // Style options: brand only makes sense when there's at least one standalone
  // single-provider button. Light only makes sense for aggregators (it's the
  // elevated white surface). Show the union; the renderer falls back gracefully.
  const styleOptions = [];
  if (hasSingle) styleOptions.push(['brand', 'Brand']);
  styleOptions.push(['neutral', 'Solid']);
  if (hasAggregator) styleOptions.push(['light', 'Light']);
  styleOptions.push(['outline', 'Outline']);

  // Auto-correct invalid style if the route mix changes.
  useEffect(() => {
    const valid = styleOptions.some(([v]) => v === config.style);
    if (!valid && styleOptions.length) set('style', styleOptions[0][0]);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [hasSingle, hasAggregator]);

  const showLogosLabel = hasAggregator ? 'Show logos' : 'Show icon';
  const showLogosHint = hasAggregator ? 'Cluster of provider logos / Mesh mark' : 'Provider mark on the left';

  return (
    <div className="customizer">

      {routes.length > 0 && updateRoute && (
        <div className="cust-section cust-section-labels">
          <div className="cust-section-head">
            <div className="cust-label">Button labels</div>
            <div className="cust-hint">One label per route. {`{provider}`} auto-fills each provider name.</div>
          </div>
          <div className="cust-route-labels">
            {routes.map(route => (
              <RouteLabelCard
                key={route.id}
                route={route}
                onChange={(label) => updateRoute(route.id, { label })}
              />
            ))}
          </div>
        </div>
      )}

      <div className="cust-section cust-section-presets">
        <div className="cust-section-head">
          <div className="cust-label">Style preset</div>
          <div className="cust-hint">Pick a starting point — fine-tune below.</div>
        </div>
        <div className="preset-grid">
          {STYLE_PRESETS.map(p => (
            <PresetCard
              key={p.id}
              preset={p}
              active={activePreset?.id === p.id}
              onClick={() => applyPreset(p)}
            />
          ))}
        </div>
      </div>

      <div className="cust-grid-2col">
        <div className="cust-section">
          <div className="cust-label">Theme</div>
          <SegControl value={config.theme} onChange={v => setStructural('theme', v)} options={[['light', 'Light'], ['dark', 'Dark']]}/>
        </div>

        <div className="cust-section">
          <div className="cust-label">Style</div>
          <SegControl value={config.style} onChange={v => setStructural('style', v)} options={styleOptions}/>
        </div>

        <div className="cust-section">
          <div className="cust-label">Size</div>
          <SegControl value={config.size} onChange={v => set('size', v)} options={[['sm', 'S'], ['md', 'M'], ['lg', 'L']]}/>
        </div>

        <div className="cust-section">
          <div className="cust-label">Shape</div>
          <SegControl value={config.shape} onChange={v => setStructural('shape', v)} options={[['square', 'Square'], ['rounded', 'Rounded'], ['pill', 'Pill']]}/>
        </div>
      </div>

      <AdvancedSection
        config={config}
        set={set}
        firstSingleProvider={firstSingleProvider}
        onlySingle={onlySingle}
      />

      <div className="cust-section cust-toggle-row">
        <div>
          <div className="cust-label">{showLogosLabel}</div>
          <div className="cust-hint">{showLogosHint}</div>
        </div>
        <Toggle on={config.showIcon} onChange={v => set('showIcon', v)}/>
      </div>
    </div>
  );
}

// ---------- Advanced (custom shape & color) section ----------
// Shows the preset-derived defaults as the "inherit" baseline for color
// pickers, so users can dial in a brand-matching button without having to
// pick a color from scratch.
function AdvancedSection({ config, set, firstSingleProvider, onlySingle }) {
  const [open, setOpen] = useState(
    // Auto-expand if any custom override is already set.
    config.customRadius != null || !!config.customBg || !!config.customFg
    || config.borderWidth != null || !!config.borderColor
  );

  // Resolve what bg/fg/border the button is using *right now* so the color
  // chips reflect the live preview baseline.
  const previewIntegration = onlySingle ? firstSingleProvider : null;
  const resolved = computeButtonStyles({
    mode: onlySingle ? 'single' : 'multi',
    integration: previewIntegration,
    config: { ...config, customBg: null, customFg: null, borderColor: null, borderWidth: null, customRadius: null },
  });
  const presetRadius = BTN_RADIUS_MAP[config.shape] || 10;

  // Count which overrides are currently active so we can show a badge and
  // surface a one-click reset right in the section header.
  const overrideCount =
    (config.customRadius != null ? 1 : 0) +
    (config.borderWidth != null ? 1 : 0) +
    (config.customBg ? 1 : 0) +
    (config.customFg ? 1 : 0) +
    (config.borderColor ? 1 : 0);
  const resetAll = () => {
    set('customRadius', null);
    set('customBg', null);
    set('customFg', null);
    set('borderWidth', null);
    set('borderColor', null);
  };

  return (
    <div className="cust-section cust-advanced">
      <div className={cx('cust-advanced-head', open && 'cust-advanced-head-open')}>
        <button
          type="button"
          className="cust-advanced-toggle"
          onClick={() => setOpen(o => !o)}
        >
          <div>
            <div className="cust-label">
              Custom shape &amp; colors
              {overrideCount > 0 && (
                <span className="cust-advanced-badge">{overrideCount}</span>
              )}
            </div>
            <div className="cust-hint">Override the preset to match your brand exactly.</div>
          </div>
          <Icon.ChevronRight size={14}/>
        </button>
        {overrideCount > 0 && (
          <button
            type="button"
            className="cust-advanced-reset-inline"
            onClick={(e) => { e.stopPropagation(); resetAll(); }}
            title="Clear all custom overrides"
          >
            Reset
          </button>
        )}
      </div>

      {open && (
        <div className="cust-advanced-body">
          <div className="cust-grid-2col">
            <NumberSlider
              label="Corner radius"
              value={config.customRadius != null ? config.customRadius : presetRadius}
              inherited={config.customRadius == null}
              inheritedHint={`Inherits ${config.shape}`}
              min={0}
              max={32}
              unit="px"
              onChange={(v) => set('customRadius', v)}
              onReset={() => set('customRadius', null)}
            />

            <NumberSlider
              label="Border width"
              value={config.borderWidth != null ? config.borderWidth : 1}
              inherited={config.borderWidth == null}
              inheritedHint="Inherits 1px"
              min={0}
              max={4}
              unit="px"
              onChange={(v) => set('borderWidth', v)}
              onReset={() => set('borderWidth', null)}
            />

            <ColorRow
              label="Background"
              value={config.customBg || normalizeHex(resolved.bg)}
              inherited={!config.customBg}
              inheritedHint={resolved.bg === 'transparent' ? 'Transparent' : `Inherits ${resolved.bg}`}
              onChange={(v) => set('customBg', v)}
              onReset={() => set('customBg', null)}
              allowTransparent={resolved.bg === 'transparent'}
            />

            <ColorRow
              label="Text"
              value={config.customFg || normalizeHex(resolved.fg)}
              inherited={!config.customFg}
              inheritedHint={`Inherits ${resolved.fg}`}
              onChange={(v) => set('customFg', v)}
              onReset={() => set('customFg', null)}
            />
          </div>

          <ColorRow
            label="Border color"
            value={config.borderColor || normalizeHex(resolved.border)}
            inherited={!config.borderColor}
            inheritedHint={resolved.border === 'transparent' ? 'Transparent' : `Inherits ${resolved.border}`}
            onChange={(v) => set('borderColor', v)}
            onReset={() => set('borderColor', null)}
            allowTransparent={resolved.border === 'transparent'}
            disabled={config.borderWidth === 0}
          />
        </div>
      )}
    </div>
  );
}

// Browser <input type="color"> only accepts #RRGGBB. Coerce common forms.
function normalizeHex(c) {
  if (!c || typeof c !== 'string') return '#000000';
  c = c.trim();
  if (c === 'transparent') return '#000000';
  if (/^#[0-9a-f]{3}$/i.test(c)) {
    return '#' + c.slice(1).split('').map(ch => ch + ch).join('').toLowerCase();
  }
  if (/^#[0-9a-f]{6}$/i.test(c)) return c.toLowerCase();
  if (/^#[0-9a-f]{8}$/i.test(c)) return c.slice(0, 7).toLowerCase();
  // Best-effort: leave anything else alone (rgba, hsl, named colors).
  return '#000000';
}

function NumberSlider({ label, value, inherited, inheritedHint, min, max, step = 1, unit = '', onChange, onReset }) {
  return (
    <div className="cust-row">
      <div className="cust-row-head">
        <span className="cust-row-label">{label}</span>
        <span className="cust-row-value">
          {inherited ? (
            <span className="cust-row-inherit">{inheritedHint}</span>
          ) : (
            <>
              {value}{unit}
              <button type="button" className="cust-row-reset" onClick={onReset} title="Reset to preset">
                <Icon.X size={11}/>
              </button>
            </>
          )}
        </span>
      </div>
      <input
        type="range"
        min={min}
        max={max}
        step={step}
        value={value}
        className="cust-slider"
        onChange={(e) => onChange(Number(e.target.value))}
      />
    </div>
  );
}

function ColorRow({ label, value, inherited, inheritedHint, onChange, onReset, allowTransparent = false, disabled = false }) {
  const hex = normalizeHex(value);
  const isTransparent = value === 'transparent' || (allowTransparent && inherited);

  return (
    <div className={cx('cust-row cust-row-color', disabled && 'cust-row-disabled')}>
      <div className="cust-row-head">
        <span className="cust-row-label">{label}</span>
        {!inherited && (
          <button type="button" className="cust-row-reset" onClick={onReset} title="Reset to preset">
            <Icon.X size={11}/>
          </button>
        )}
      </div>
      <div className="cust-color-input">
        <label className="cust-color-swatch" style={{
          background: isTransparent
            ? 'repeating-conic-gradient(#e5e7eb 0deg 90deg, #fff 90deg 180deg) 0 0 / 8px 8px'
            : hex,
        }}>
          <input
            type="color"
            value={hex}
            disabled={disabled}
            onChange={(e) => onChange(e.target.value.toLowerCase())}
          />
        </label>
        <input
          type="text"
          className="cust-color-text"
          value={inherited ? '' : hex}
          placeholder={inherited ? inheritedHint : '#000000'}
          disabled={disabled}
          onChange={(e) => {
            const v = e.target.value.trim();
            if (v === '') { onReset(); return; }
            if (/^#?[0-9a-f]{3}$|^#?[0-9a-f]{6}$/i.test(v)) {
              onChange(v.startsWith('#') ? v.toLowerCase() : '#' + v.toLowerCase());
            }
          }}
        />
      </div>
    </div>
  );
}

// Mini preview swatch for a style preset. Renders a stripped-down version of
// the button using the preset's config so users can visually scan options.
function PresetCard({ preset, active, onClick }) {
  const { theme, style, size, shape, showIcon } = preset.config;
  const isDark = theme === 'dark';

  // Resolve preview swatch styles inline — mirrors computeButtonStyles but
  // simplified for static display (no real provider, just a generic look).
  let bg = '#ffffff', fg = '#0f172a', border = '1px solid rgba(15,23,42,0.10)';
  if (style === 'brand')   { bg = '#0052ff';                   fg = '#ffffff'; border = '1px solid transparent'; }
  if (style === 'neutral') { bg = isDark ? '#0f172a' : '#0f172a'; fg = '#ffffff'; border = '1px solid transparent'; }
  if (style === 'light')   { bg = '#ffffff';                   fg = '#0f172a'; border = '1px solid rgba(15,23,42,0.10)'; }
  if (style === 'outline') { bg = 'transparent';               fg = isDark ? '#ffffff' : '#0f172a'; border = `1.5px solid ${isDark ? 'rgba(255,255,255,0.4)' : 'rgba(15,23,42,0.25)'}`; }

  const radius = shape === 'square' ? 4 : shape === 'pill' ? 999 : 8;
  const stageBg = isDark ? '#0a0e1a' : '#ffffff';

  return (
    <button
      className={cx('preset-card', active && 'preset-card-active')}
      onClick={onClick}
      title={preset.name}
    >
      <div className="preset-stage" style={{ background: stageBg }}>
        <div
          className="preset-btn"
          style={{
            background: bg,
            color: fg,
            border,
            borderRadius: radius,
          }}
        >
          {showIcon && <span className="preset-btn-dot" style={{ background: fg, opacity: 0.85 }}/>}
          <span className="preset-btn-label">Deposit</span>
        </div>
      </div>
      <div className="preset-name">{preset.name}</div>
    </button>
  );
}

function RouteLabelCard({ route, onChange }) {
  const identity = (() => {
    if (route.kind === 'single') {
      const integ = route.selected?.[0] || null;
      const count = route.selected?.length || 0;
      return {
        title: 'Quick deposit',
        sub: count === 0
          ? 'Standalone buttons'
          : count === 1 && integ
            ? `Standalone — ${integ.name}`
            : `${count} standalone buttons`,
      };
    }
    if (route.kind === 'multi') {
      return {
        title: 'Direct deposit',
        sub: `${route.selected?.length || 0} provider${(route.selected?.length || 0) === 1 ? '' : 's'} aggregated`,
      };
    }
    if (route.kind === 'category') {
      return {
        title: CATEGORY_LABELS[route.category] || 'Category',
        sub: 'Category-scoped button',
      };
    }
    return { title: 'Route', sub: '' };
  })();

  // Lifecycle-specific defaults + chip suggestions.
  const presets = (() => {
    if (route.kind === 'single') {
      return ['Deposit with {provider}', '{provider}', 'Quick deposit', 'Connect {provider}'];
    }
    if (route.kind === 'multi') {
      return ['Smart deposit', 'Secure deposit', 'Direct deposit', 'Connect your account'];
    }
    if (route.kind === 'category') {
      if (route.category === 'exchange') return ['Deposit from an exchange', 'Connect your exchange', 'Deposit'];
      if (route.category === 'deFiWallet') return ['Deposit from a wallet', 'Connect your wallet', 'Deposit'];
      return ['Deposit', 'Connect account'];
    }
    return [];
  })();

  const placeholder = (() => {
    if (route.kind === 'single') {
      const integ = route.selected?.[0] || null;
      return integ ? `Deposit with ${integ.name}` : 'Deposit with {provider}';
    }
    if (route.kind === 'multi') return 'Direct deposit';
    if (route.kind === 'category') {
      if (route.category === 'exchange') return 'Deposit from an exchange';
      if (route.category === 'deFiWallet') return 'Deposit from a wallet';
    }
    return 'Deposit';
  })();

  return (
    <div className="rlc">
      <div className="rlc-head">
        <div className="rlc-meta">
          <div className="rlc-title">{identity.title}</div>
          <div className="rlc-sub">{identity.sub}</div>
        </div>
      </div>
      <input
        className="rlc-input"
        placeholder={placeholder}
        value={route.label || ''}
        onChange={e => onChange(e.target.value)}
      />
      <div className="rlc-presets">
        {presets.map(p => (
          <button
            key={p}
            type="button"
            className={cx('rlc-preset', route.label === p && 'rlc-preset-active')}
            onClick={() => onChange(p)}
          >{p}</button>
        ))}
        {route.label && (
          <button
            type="button"
            className="rlc-preset rlc-preset-clear"
            onClick={() => onChange('')}
            title="Use the default"
          >Clear</button>
        )}
      </div>
    </div>
  );
}

function SegControl({ value, onChange, options }) {
  return (
    <div className="seg">
      {options.map(([v, label]) => (
        <button
          key={v}
          className={cx('seg-btn', value === v && 'seg-btn-active')}
          onClick={() => onChange(v)}
        >
          {label}
        </button>
      ))}
    </div>
  );
}

function Toggle({ on, onChange }) {
  return (
    <button className={cx('toggle', on && 'toggle-on')} onClick={() => onChange(!on)}>
      <span className="toggle-knob"/>
    </button>
  );
}

// ---------- Main App ----------
// Route model: the user composes 1-3 "routes" (entry points). Each route is one
// of: 'single' (standalone, one button per selected provider), 'multi' (one
// aggregator button across selected providers), or 'category' (one button
// scoped to a category). Routes are configured independently, then share one
// visual style.
const newRoute = (kind) => ({
  id: `r-${Math.random().toString(36).slice(2, 8)}`,
  kind, // 'single' | 'multi' | 'category'
  selected: [],
  category: null,
  label: '', // empty = use lifecycle default at render time
});

function App() {
  const [step, setStep] = useState(0);
  // Routes the user wants to ship. Order = render order in preview/export.
  const [routes, setRoutes] = useState([]);
  // Index of the currently active config tab in step 1 (provider/category picker).
  const [activeRouteIdx, setActiveRouteIdx] = useState(0);
  const [config, setConfig] = useState({
    label: '',
    theme: 'light',
    style: 'brand',
    size: 'md',
    shape: 'rounded',
    showIcon: true,
    // Advanced overrides — null = inherit from style/shape presets above.
    customRadius: null,    // number (px) overrides BTN_RADIUS_MAP[shape]
    customBg: null,        // hex string overrides resolved bg
    customFg: null,        // hex string overrides resolved fg
    borderWidth: null,     // number (px) overrides default 1px
    borderColor: null,     // hex string overrides resolved border color
  });
  const [surface, setSurface] = useState('card'); // 'card' | 'exchange' | 'fintech' | 'checkout'
  const [modalOpen, setModalOpen] = useState(false);
  const [modalContext, setModalContext] = useState(null); // { route, integration? }
  const [exportOpen, setExportOpen] = useState(false);

  // Sample integrations for mode previews.
  const sample = useMemo(() => {
    return uniqByName(window.INTEGRATIONS)
      .filter(i => i.name !== 'PayPal' && pickIcon(i))
      .sort(byRank)
      .slice(0, 6);
  }, []);

  const hasRoute = (kind) => routes.some(r => r.kind === kind);
  const toggleRoute = (kind) => {
    setRoutes(prev => {
      const exists = prev.find(r => r.kind === kind);
      if (exists) return prev.filter(r => r.id !== exists.id);
      if (prev.length >= 3) return prev; // hard cap
      return [...prev, newRoute(kind)];
    });
  };

  const updateRoute = (id, patch) => {
    setRoutes(prev => prev.map(r => r.id === id ? { ...r, ...patch } : r));
  };

  const toggleIntegrationOnRoute = (routeId, i) => {
    setRoutes(prev => prev.map(r => {
      if (r.id !== routeId) return r;
      const has = r.selected.some(s => s.id === i.id);
      return { ...r, selected: has ? r.selected.filter(s => s.id !== i.id) : [...r.selected, i] };
    }));
  };

  // Resolve a route's effective provider list (category routes => providers in category).
  const resolveRouteProviders = (route) => {
    if (route.kind === 'category' && route.category) {
      return uniqByName(
        window.INTEGRATIONS.filter(i => i.category === route.category && pickIcon(i))
      ).sort(byRank);
    }
    return route.selected;
  };

  const stepLabels = ['Routes', 'Configure', 'Customize & embed'];
  const canProceedStep0 = routes.length > 0;
  const isRouteConfigured = (r) => {
    if (r.kind === 'category') return !!r.category;
    return r.selected.length > 0;
  };
  const canProceedStep1 = routes.length > 0 && routes.every(isRouteConfigured);

  // Code emits all routes at once. Three flavors for the export tabs.
  const codeSnippets = useMemo(() => ([
    { id: 'html', label: 'HTML', code: generateEmbed({ routes, config, resolveRouteProviders }) },
    { id: 'react', label: 'React', code: generateReactEmbed({ routes, config, resolveRouteProviders }) },
    { id: 'vue', label: 'Vue', code: generateVueEmbed({ routes, config, resolveRouteProviders }) },
  ]), [routes, config, resolveRouteProviders]);

  return (
    <div className="app">
      <Header step={step} setStep={setStep} stepLabels={stepLabels}/>

      <main className="main">
        {step === 0 && (
          <div className="container">
            <div className="hero">
              <div className="hero-eyebrow"><Icon.Sparkle size={12}/> Mesh deposit button builder</div>
              <h1>Compose your deposit experience.</h1>
              <p>Mesh deposit buttons let users move funds from 300+ exchanges and wallets in one click — faster, safer, and higher-converting than QR codes alone. Pick up to three lifecycle stages below to compose a complete deposit page.</p>
            </div>

            <div className="mode-grid">
              {MODES.map(m => {
                const picked = hasRoute(m.id);
                const disabled = !picked && routes.length >= 3;
                return (
                  <ModeCard
                    key={m.id}
                    mode={m}
                    active={picked}
                    disabled={disabled}
                    onClick={() => toggleRoute(m.id)}
                    sample={sample}
                  />
                );
              })}
            </div>

            {routes.length === 0 && (
              <div className="pairing-hint">
                <div className="pairing-hint-icon"><Icon.Sparkle size={14}/></div>
                <div className="pairing-hint-body">
                  <div className="pairing-hint-title">Recommended for deposit pages</div>
                  <div className="pairing-hint-desc">
                    Pair <strong>Direct deposit</strong> for first-time users with <strong>Quick deposit</strong> for returning users — Mesh customers see 20–30% higher conversion vs. QR-only flows.
                  </div>
                </div>
                <button
                  className="pairing-hint-action"
                  onClick={() => { toggleRoute('multi'); toggleRoute('single'); }}
                >
                  Use this pairing
                </button>
              </div>
            )}

            <div className="route-cart">
              <div className="route-cart-info">
                <div className="route-cart-label">Routes selected</div>
                <div className="route-cart-count">{routes.length}<span className="route-cart-cap"> / 3</span></div>
                {routes.length > 0 && (
                  <div className="route-cart-list">
                    {routes.map(r => (
                      <span key={r.id} className="route-chip">
                        {MODES.find(m => m.id === r.kind).title}
                        <button className="route-chip-x" onClick={() => toggleRoute(r.kind)} aria-label="Remove"><Icon.X size={10}/></button>
                      </span>
                    ))}
                  </div>
                )}
              </div>
              <button
                className="btn-primary"
                disabled={!canProceedStep0}
                onClick={() => { setActiveRouteIdx(0); setStep(1); }}
              >
                Continue <Icon.ChevronRight size={14}/>
              </button>
            </div>
          </div>
        )}

        {step === 1 && (() => {
          const activeRoute = routes[activeRouteIdx] || routes[0];
          const labelFor = (k) => MODES.find(m => m.id === k).title;
          return (
            <div className="container">
              <div className="step-head">
                <div>
                  <div className="step-eyebrow">Step 2</div>
                  <h2>{routes.length > 1 ? 'Configure each route' : 'Configure your route'}</h2>
                  <p>{routes.length > 1 ? 'Switch between routes using the tabs below. Style and code come next.' : 'Set up the providers (or category). Style and code come next.'}</p>
                </div>
                <div className="step-actions">
                  <button className="btn-ghost" onClick={() => setStep(0)}><Icon.ChevronLeft size={14}/> Back</button>
                  <button className="btn-primary" disabled={!canProceedStep1} onClick={() => setStep(2)}>
                    Continue <Icon.ChevronRight size={14}/>
                  </button>
                </div>
              </div>

              {routes.length > 1 && (
                <div className="route-tabs">
                  {routes.map((r, idx) => {
                    const ok = isRouteConfigured(r);
                    return (
                      <button
                        key={r.id}
                        className={cx('route-tab', idx === activeRouteIdx && 'route-tab-active')}
                        onClick={() => setActiveRouteIdx(idx)}
                      >
                        <span className={cx('route-tab-dot', ok && 'route-tab-dot-ok')}/>
                        <span className="route-tab-label">{labelFor(r.kind)}</span>
                        <span className="route-tab-meta">
                          {r.kind === 'category' ? (r.category ? CATEGORY_LABELS[r.category] : 'Choose category') : `${r.selected.length} selected`}
                        </span>
                      </button>
                    );
                  })}
                </div>
              )}

              {activeRoute && (
                <div className="route-active-head">
                  <div className="route-active-title">{labelFor(activeRoute.kind)}</div>
                  <div className="route-active-desc">
                    {activeRoute.kind === 'single' && 'Pick one or more providers — each becomes its own standalone button.'}
                    {activeRoute.kind === 'multi' && 'Pick the providers users will see in this aggregator. Their logos cluster together on a single button.'}
                    {activeRoute.kind === 'category' && 'Choose which Mesh-curated category this button opens.'}
                  </div>
                </div>
              )}

              {activeRoute && activeRoute.kind === 'category' ? (
                <CategoryPicker
                  category={activeRoute.category}
                  onPick={(cat) => updateRoute(activeRoute.id, { category: cat })}
                />
              ) : activeRoute ? (
                <IntegrationCatalog
                  integrations={window.INTEGRATIONS}
                  selected={activeRoute.selected}
                  onToggle={(i) => toggleIntegrationOnRoute(activeRoute.id, i)}
                  mode={activeRoute.kind}
                  maxSelect={null}
                />
              ) : null}
            </div>
          );
        })()}

        {step === 2 && (() => {
          const totalReach = routes.reduce((sum, r) => sum + resolveRouteProviders(r).length, 0);
          return (
            <div className="container container-wide">
              <div className="step-head">
                <div>
                  <div className="step-eyebrow">Step 3</div>
                  <h2>Make it yours</h2>
                  <p>One style, applied to every deposit button. Place above your QR code for the highest conversion.</p>
                </div>
                <div className="step-actions">
                  <button className="btn-ghost" onClick={() => setStep(1)}><Icon.ChevronLeft size={14}/> Back</button>
                </div>
              </div>

              <div className="builder builder-2col">
                <div className="builder-left">
                  <Customizer config={config} setConfig={setConfig} routes={routes} resolveRouteProviders={resolveRouteProviders} updateRoute={updateRoute}/>
                </div>

                <div className="builder-center">
                  <div className="preview-tabs preview-tabs-surfaces">
                    {[
                      ['card', 'Card', <Icon.Card key="i" size={13}/>],
                      ['exchange', 'Exchange', <Icon.Exchange key="i" size={13}/>],
                      ['fintech', 'Fintech app', <Icon.Phone key="i" size={13}/>],
                      ['checkout', 'Checkout', <Icon.Checkout key="i" size={13}/>],
                    ].map(([id, label, icon]) => (
                      <button
                        key={id}
                        className={cx('preview-tab', surface === id && 'preview-tab-active')}
                        onClick={() => setSurface(id)}
                      >
                        {icon}
                        <span>{label}</span>
                      </button>
                    ))}
                  </div>

                  <div className={cx('preview-stage', `preview-stage-${surface}`, config.theme === 'dark' && 'preview-stage-dark')}>
                    {(() => {
                      const renderButtons = () => (
                        <RouteStack routes={routes} config={config} resolveRouteProviders={resolveRouteProviders} onLaunch={(ctx) => { setModalContext(ctx); setModalOpen(true); }}/>
                      );
                      if (surface === 'card') {
                        return (
                          <div className="preview-isolated">
                            {renderButtons()}
                            <div className="preview-hint">Click any button to preview the launch →</div>
                          </div>
                        );
                      }
                      if (surface === 'exchange') return <SurfaceExchange dark={config.theme === 'dark'}>{renderButtons()}</SurfaceExchange>;
                      if (surface === 'fintech') return <SurfaceFintech dark={config.theme === 'dark'}>{renderButtons()}</SurfaceFintech>;
                      if (surface === 'checkout') return <SurfaceCheckout dark={config.theme === 'dark'}>{renderButtons()}</SurfaceCheckout>;
                      return null;
                    })()}
                  </div>

                  <div className="preview-summary">
                    <SummaryStat label="Routes" value={`${routes.length} of 3`}/>
                    <SummaryStat label="Theme" value={config.theme === 'dark' ? 'Dark' : 'Light'}/>
                    <SummaryStat label="Total reach" value={`${totalReach} provider${totalReach === 1 ? '' : 's'}`}/>
                  </div>

                  <div className="export-cta-wrap">
                    <button className="btn-export" onClick={() => setExportOpen(true)}>
                      <Icon.Code size={16}/>
                      <span>Export code</span>
                      <span className="btn-export-meta">HTML · React · Vue</span>
                    </button>
                    <div className="export-cta-hint">Ready when you are. Grab the snippet for your stack.</div>
                  </div>
                </div>
              </div>
            </div>
          );
        })()}
      </main>

      <MeshLinkModal
        open={modalOpen}
        integrations={(() => {
          if (!modalContext) return sample;
          const { route, integration } = modalContext;
          if (integration) return [integration];
          if (route) return resolveRouteProviders(route);
          return sample;
        })()}
        mode={modalContext?.route?.kind || 'multi'}
        onClose={() => setModalOpen(false)}
      />

      <ExportModal
        open={exportOpen}
        snippets={codeSnippets}
        onClose={() => setExportOpen(false)}
      />
    </div>
  );
}

function SummaryStat({ label, value }) {
  return (
    <div className="sum-stat">
      <div className="sum-label">{label}</div>
      <div className="sum-value">{value}</div>
    </div>
  );
}

function CategoryPicker({ category, onPick }) {
  const categories = [
    {
      id: 'exchange',
      name: 'Exchanges & Brokerages',
      desc: 'Centralized providers — Coinbase, Binance, Kraken, Robinhood, Gemini, Bitstamp, and more.',
    },
    {
      id: 'deFiWallet',
      name: 'Self-Custody Wallets',
      desc: 'Software and hardware wallets — MetaMask, Phantom, Ledger, Trezor, and 100+ more.',
    },
  ];

  return (
    <div className="cat-grid">
      {categories.map(c => {
        const list = uniqByName(window.INTEGRATIONS.filter(i => i.category === c.id && pickIcon(i))).sort(byRank);
        const active = category === c.id;
        return (
          <button key={c.id} className={cx('cat-card', active && 'cat-card-active')} onClick={() => onPick(c.id)}>
            <div className="cat-head">
              <div>
                <div className="cat-name">{c.name}</div>
                <div className="cat-desc">{c.desc}</div>
              </div>
              <div className="cat-radio"><div className="cat-radio-inner"/></div>
            </div>
            <div className="cat-count">{list.length} providers included</div>
            <div className="cat-logos">
              {list.slice(0, 24).map(i => (
                <div key={i.id} className="cat-logo">
                  <img src={pickIcon(i, 'light')} alt={i.name} loading="lazy"/>
                </div>
              ))}
              {list.length > 24 && <div className="cat-logo cat-logo-more">+{list.length - 24}</div>}
            </div>
          </button>
        );
      })}
    </div>
  );
}

function Header({ step, setStep, stepLabels }) {
  return (
    <header className="header">
      <div className="header-inner">
        <div className="brand">
          <img src="assets/mesh-logo.png" alt="Mesh" className="brand-logo"/>
          <div className="brand-text">
            <div className="brand-sub">Deposit Button Builder</div>
          </div>
        </div>
        <Steps current={step} steps={stepLabels}/>
      </div>
    </header>
  );
}

window.App = App;
