// ---- UI components for the Domain Ideas Generator ----
const { useState, useEffect, useRef } = React;

function InfoTip({ text }) {
  return (
    <span className="infotip" tabIndex={0}>
      <span className="infotip-icon" aria-hidden="true">i</span>
      <span className="infotip-bubble" role="tooltip">{text}</span>
    </span>
  );
}

function OptionToggle({ label, tip, value, onChange }) {
  return (
    <label className="opt-toggle">
      <button
        type="button"
        className={'switch' + (value ? ' switch-on' : '')}
        role="switch"
        aria-checked={value}
        onClick={() => onChange(!value)}
      ><span className="knob"></span></button>
      <span className="opt-label">{label}</span>
      <InfoTip text={tip} />
    </label>
  );
}

function PromptBox({ prompt, setPrompt, onGenerate, loading, onlyAvailable, setOnlyAvailable, socialCheck, setSocialCheck, credits, canAfford, onTopUp }) {
  const ref = useRef(null);
  useEffect(() => { if (ref.current) ref.current.focus(); }, []);
  const handleKey = (e) => {
    if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); onGenerate(); }
  };
  return (
    <div className="prompt-card">
      <textarea
        ref={ref}
        className="prompt-input"
        rows={4}
        placeholder="Describe your idea… e.g. “a journaling app for busy parents”"
        value={prompt}
        onChange={(e) => setPrompt(e.target.value)}
        onKeyDown={handleKey}
      ></textarea>
      <div className="prompt-options">
        <OptionToggle
          label="Only available"
          tip="Show only domains that are available to register right now."
          value={onlyAvailable}
          onChange={setOnlyAvailable}
        />
        <OptionToggle
          label="Social handles"
          tip="Also check if matching social media handles (X, Instagram) are free. Uses 50 extra credits."
          value={socialCheck}
          onChange={setSocialCheck}
        />
        {canAfford === false ? (
          <button className="credits credits-out" onClick={onTopUp}>
            <span className="credit-coin credit-coin-out" aria-hidden="true"></span>
            Not enough credits — top up
          </button>
        ) : (
          <span className="credits" title="Credits used per generation">
            <span className="credit-coin" aria-hidden="true"></span>
            {credits} credit{credits > 1 ? 's' : ''} per search
          </span>
        )}
      </div>
      <div className="prompt-footer">
        <span className="hint">Press Enter to generate</span>
        <button className="btn-primary" onClick={onGenerate} disabled={loading || !prompt.trim() || canAfford === false}>
          {loading ? 'Thinking…' : 'Find domains'}
        </button>
      </div>
    </div>
  );
}

function TldChips({ selected, setSelected }) {
  const [expanded, setExpanded] = useState(false);
  const isAny = selected.length === 0;
  const VISIBLE = 9;
  const shown = expanded ? TLDS : TLDS.slice(0, VISIBLE);
  const hiddenSelected = !expanded && selected.some(ext => !TLDS.slice(0, VISIBLE).find(t => t.ext === ext));
  const toggle = (ext) => {
    if (selected.includes(ext)) setSelected(selected.filter(e => e !== ext));
    else setSelected([...selected, ext]);
  };
  return (
    <div className="chips" role="group" aria-label="Domain extensions">
      <button className={'chip' + (isAny ? ' chip-on' : '')} onClick={() => setSelected([])}>Any</button>
      {shown.map(t => (
        <button
          key={t.ext}
          className={'chip' + (selected.includes(t.ext) ? ' chip-on' : '')}
          onClick={() => toggle(t.ext)}
        >{t.ext}</button>
      ))}
      {/* keep hidden-but-selected chips visible when collapsed */}
      {!expanded && hiddenSelected ? TLDS.slice(VISIBLE).filter(t => selected.includes(t.ext)).map(t => (
        <button key={t.ext} className="chip chip-on" onClick={() => toggle(t.ext)}>{t.ext}</button>
      )) : null}
      <button className="chip chip-more" onClick={() => setExpanded(!expanded)}>
        {expanded ? 'Show less' : '+' + (TLDS.length - VISIBLE) + ' more'}
      </button>
    </div>
  );
}

function SkeletonRows({ count = 10 }) {
  return (
    <div className="results">
      {Array.from({ length: count }).map((_, i) => (
        <div className="row row-skeleton" key={i} style={{ animationDelay: (i * 80) + 'ms' }}>
          <div className="sk sk-name"></div>
          <div className="sk sk-price"></div>
        </div>
      ))}
    </div>
  );
}

function HandleChip({ network, free }) {
  return (
    <span className={'handle-chip' + (free ? ' handle-free' : ' handle-taken')}
          title={network + ' handle ' + (free ? 'available' : 'taken')}>
      <span className="dot"></span>{network}
    </span>
  );
}

function ResultRow({ d, index, status, onBuy, onHandles, isFav, onFav, onAvail }) {
  const checking = status === 'checking';
  const taken = status === 'taken';
  const available = status === 'available';
  const none = status === 'none';
  const unknown = status === 'unknown';
  const [reviewOpen, setReviewOpen] = useState(false);
  const [popStyle, setPopStyle] = useState(null);
  const chipRef = React.useRef(null);
  // Score is a genuine AI judgement only — if the AI review is unavailable we
  // simply don't show a score chip (never a local heuristic under an AI label).
  const score = (status === 'checking' || !d.review) ? null : d.review.score;
  // Position the popover with fixed coordinates derived from the chip so it
  // escapes the results container's `overflow: hidden` and is never clipped —
  // the top few rows used to lose the upward popover off the frame. Prefer
  // opening below (always room) and only flip up when below is tight.
  // Mirrors the Domain Hunt rows.
  const toggleReview = () => {
    setReviewOpen(o => {
      if (!o && chipRef.current) {
        const rect = chipRef.current.getBoundingClientRect();
        const narrow = window.innerWidth < 560;
        const roomBelow = window.innerHeight - rect.bottom;
        const up = roomBelow < 320 && rect.top > 320;
        const s = { position: 'fixed', zIndex: 250 };
        if (narrow) {
          s.left = '12px'; s.right = '12px'; s.width = 'auto';
        } else {
          s.width = '290px';
          s.right = Math.max(12, Math.round(window.innerWidth - rect.right)) + 'px';
        }
        if (up) { s.bottom = Math.round(window.innerHeight - rect.top + 8) + 'px'; s.top = 'auto'; }
        else { s.top = Math.round(rect.bottom + 8) + 'px'; s.bottom = 'auto'; }
        setPopStyle(s);
      }
      return !o;
    });
  };
  return (
    <div className={'row-block' + (reviewOpen ? ' row-block-pop' : '')} style={{ animationDelay: (index * 45) + 'ms' }}>
      <div className={'row' + (taken ? ' row-taken' : '')}>
        <div className="row-main">
          <span className="domain-name">
            {d.display.slice(0, d.display.length - d.ext.length)}
            <span className="domain-ext">{d.ext}</span>
          </span>
          {none ? (
            <button className="badge-avail-check" onClick={() => onAvail(d)}
                    title="Check availability — 10 credits">
              Check availability
            </button>
          ) : checking ? (
            <span className="badge-checking">
              <span className="mini-spinner" aria-hidden="true"></span>Checking…
            </span>
          ) : taken ? (
            <span className="badge-available badge-taken">
              <span className="dot"></span>Taken
            </span>
          ) : unknown ? (
            <span className="badge-available badge-unknown">
              <span className="dot"></span>Unknown
            </span>
          ) : (
            <span className="badge-available">
              <span className="dot"></span>Available
            </span>
          )}
        </div>
        <div className="row-side">
          {score != null ? (
            <div className="score-wrap">
              <button ref={chipRef} className={'score-chip' + (reviewOpen ? ' score-chip-on' : '')} onClick={toggleReview} title="AI name review — click for details" aria-expanded={reviewOpen}>
                <span className="score-spark" aria-hidden="true">✦</span>{score.toFixed(1)}
              </button>
              {reviewOpen && window.ReactDOM && window.ReactDOM.createPortal
                ? window.ReactDOM.createPortal(
                  <React.Fragment>
                    <div className="score-pop-scrim" style={{ zIndex: 249 }} onClick={() => setReviewOpen(false)}></div>
                    <div className="score-pop" style={popStyle || undefined}>
                      <NameReview domain={d} defaultOpen compact />
                    </div>
                  </React.Fragment>,
                  document.body
                )
                : null}
            </div>
          ) : null}
          <button
            className={'fav-btn' + (isFav ? ' fav-on' : '')}
            onClick={() => onFav(d)}
            title={isFav ? 'Remove from favorites' : 'Add to favorites'}
            aria-pressed={isFav}
          >{isFav ? '\u2665' : '\u2661'}</button>
          <button
            className="handle-btn"
            onClick={() => onHandles(d)}
            title="Check social handle availability"
          >@</button>
          {taken ? null : (
            <button className="btn-buy" onClick={() => (available || none || unknown) && onBuy(d)} disabled={!(available || none || unknown)}>
              Buy now
            </button>
          )}
        </div>
      </div>
    </div>
  );
}

function HandlesBody({ domain, onOutcome }) {
  // Real per-platform availability from the backend. null = loading,
  // 'error' = request failed, otherwise an array of platform verdicts.
  const [state, setState] = useState({ status: 'loading', list: [] });
  const report = (ok) => { if (typeof onOutcome === 'function') onOutcome(domain.id, ok); };
  useEffect(() => {
    let alive = true;
    // Use a genuine cached result (e.g. pre-fetched when the toggle was on) for
    // an instant, real view; only hit the network when nothing is cached.
    const cached = window.getHandleCache ? window.getHandleCache(domain.name) : null;
    if (cached && Array.isArray(cached.list) && cached.list.length) {
      setState({ status: 'done', list: cached.list });
      report(true);
      return () => { alive = false; };
    }
    setState({ status: 'loading', list: [] });
    fetch('/api/domains/handles', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ name: domain.name }),
    })
      .then(async (resp) => {
        const data = await resp.json().catch(() => ({}));
        if (!alive) return;
        const handles = Array.isArray(data.handles) ? data.handles : [];
        if (!resp.ok || handles.length === 0) { setState({ status: 'error', list: [] }); report(false); return; }
        if (window.setHandleCache) window.setHandleCache(domain.name, handles);
        if (window.namitEnrich) window.namitEnrich(domain.id, { handles });
        setState({ status: 'done', list: handles });
        report(true);
      })
      .catch(() => { if (alive) { setState({ status: 'error', list: [] }); report(false); } });
    return () => { alive = false; };
  }, [domain.id]);

  const { status, list } = state;
  const loading = status === 'loading';
  const freeCount = list.filter(h => h.available === true).length;
  const unknownCount = list.filter(h => h.available === null || h.available === undefined).length;

  let summary;
  if (loading) {
    summary = <span className="hp-checking"><span className="mini-spinner" aria-hidden="true"></span>Checking platforms…</span>;
  } else if (status === 'error') {
    summary = "Couldn't check handles right now. Please try again.";
  } else {
    summary = freeCount + ' of ' + list.length + ' platforms have this handle available.'
      + (unknownCount ? ' ' + unknownCount + ' couldn’t be verified.' : '');
  }

  return (
    <React.Fragment>
      <div className="panel-head">
        <div>
          <div className="panel-kicker">Social handles</div>
          <div className="panel-domain">@{domain.name}</div>
        </div>
      </div>
      <p className="panel-sub">{summary}</p>
      {loading ? (
        <div className="hp-grid hp-grid-panel">
          {Array.from({ length: 14 }).map((_, i) => (
            <div className="hp-row hp-row-checking" key={i}>
              <span className="hp-dot-spin" aria-hidden="true"></span>
              <span className="hp-platform hp-platform-skel"></span>
              <span className="hp-status hp-checking-status">Checking…</span>
            </div>
          ))}
        </div>
      ) : status === 'error' ? null : (
        <div className="hp-grid hp-grid-panel">
          {list.map((h) => {
            const free = h.available === true;
            const taken = h.available === false;
            const unknown = !free && !taken;
            const dotClass = free ? 'dot-free' : taken ? 'dot-taken' : 'dot-unknown';
            const statusLabel = free ? 'Available' : taken ? 'Taken' : 'Unknown';
            const lowConf = !unknown && h.confidence === 'medium';
            return (
              <div className="hp-row" key={h.platform}>
                <span className={'dot ' + dotClass}></span>
                <span className="hp-platform">
                  {(taken && h.profileUrl)
                    ? <a className="hp-link" href={h.profileUrl} target="_blank" rel="noopener noreferrer">{h.platform}</a>
                    : h.platform}
                </span>
                <span className={'hp-status' + (free ? ' hp-free' : '') + (unknown ? ' hp-unknown' : '')}
                      title={lowConf ? 'Lower-confidence result' : (unknown ? 'Could not verify' : '')}>
                  {statusLabel}{lowConf ? ' *' : ''}
                </span>
              </div>
            );
          })}
        </div>
      )}
      <p className="panel-foot">Live availability. Results marked “*” are lower-confidence; “Unknown” means a platform couldn’t be verified just now. Claim handles early to keep your brand consistent.</p>
    </React.Fragment>
  );
}

function AvailBody({ domain, onOutcome }) {
  // Authoritative single-domain availability from the registry (RDAP/WHOIS).
  // null = loading, 'error' = request failed, otherwise a verdict object.
  const [state, setState] = useState({ status: 'loading', available: null });
  const report = (ok, available) => { if (typeof onOutcome === 'function') onOutcome(domain.id, ok, available); };
  useEffect(() => {
    let alive = true;
    setState({ status: 'loading', available: null });
    fetch('/api/domains/check', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ domain: (domain.name + domain.ext).toLowerCase() }),
    })
      .then(async (resp) => {
        const data = await resp.json().catch(() => ({}));
        if (!alive) return;
        if (!resp.ok) { setState({ status: 'error', available: null }); report(false); return; }
        const available = (data && typeof data.available === 'boolean') ? data.available : null;
        setState({ status: 'done', available });
        report(true, available);
      })
      .catch(() => { if (alive) { setState({ status: 'error', available: null }); report(false); } });
    return () => { alive = false; };
  }, [domain.id]);

  const { status, available } = state;
  const loading = status === 'loading';
  const free = available === true;
  const taken = available === false;

  // When the live registry confirms the domain IS available, skip the bare
  // "Available" card and drop the user straight into the full buy panel
  // (registrar price comparison). Taken / unknown / error keep the verdict view
  // below. This applies everywhere the avail panel is used (names, favorites,
  // history) since they all share this component.
  if (status === 'done' && free) {
    return <VendorBody domain={domain} />;
  }

  let summary;
  if (loading) {
    summary = <span className="hp-checking"><span className="mini-spinner" aria-hidden="true"></span>Checking the registry…</span>;
  } else if (status === 'error') {
    summary = "Couldn't check availability right now. Please try again.";
  } else if (free) {
    summary = 'Good news — this domain is available to register.';
  } else if (taken) {
    summary = 'This domain is already registered.';
  } else {
    summary = "The registry didn't return a definitive answer for this domain.";
  }

  return (
    <React.Fragment>
      <div className="panel-head">
        <div>
          <div className="panel-kicker">Domain availability</div>
          <div className="panel-domain">{domain.display}</div>
        </div>
      </div>
      <p className="panel-sub">{summary}</p>
      {loading ? (
        <div className="hp-grid hp-grid-panel">
          <div className="hp-row hp-row-checking">
            <span className="hp-dot-spin" aria-hidden="true"></span>
            <span className="hp-platform hp-platform-skel"></span>
            <span className="hp-status hp-checking-status">Checking…</span>
          </div>
        </div>
      ) : status === 'error' ? null : (
        <div className="hp-grid hp-grid-panel">
          <div className="hp-row">
            <span className={'dot ' + (free ? 'dot-free' : taken ? 'dot-taken' : 'dot-unknown')}></span>
            <span className="hp-platform">{domain.display}</span>
            <span className={'hp-status' + (free ? ' hp-free' : '') + (!free && !taken ? ' hp-unknown' : '')}>
              {free ? 'Available' : taken ? 'Taken' : 'Unknown'}
            </span>
          </div>
        </div>
      )}
      <p className="panel-foot">Live registry check. “Unknown” means the registry couldn’t be verified just now. Grab available names early before someone else does.</p>
    </React.Fragment>
  );
}

function CheckAvailPrompt({ domain, cost, canAfford, onCheck, errored }) {
  return (
    <React.Fragment>
      <div className="panel-head">
        <div>
          <div className="panel-kicker">Domain availability</div>
          <div className="panel-domain">{domain.display}</div>
        </div>
      </div>
      <div className="gh-prompt">
        <div className="gh-icon" aria-hidden="true">?</div>
        <div className="gh-title">{errored ? 'That check didn’t go through' : 'Availability not checked yet'}</div>
        {errored
          ? <p className="gh-text">We couldn’t reach the registry for <strong>{domain.display}</strong>, so <strong>your credit wasn’t used</strong>. You can try again.</p>
          : <p className="gh-text">You saved <strong>{domain.display}</strong> without checking availability. Check it live against the registry now.</p>}
        <button className="btn-primary gh-btn" onClick={onCheck} disabled={!canAfford}>
          {errored ? 'Try again · ' : 'Check availability · '}{cost} credit{cost > 1 ? 's' : ''}
        </button>
        {!canAfford ? <p className="gh-note">Not enough credits — top up to continue.</p> : null}
      </div>
    </React.Fragment>
  );
}

function Stars({ rating }) {
  return (
    <span className="stars" aria-label={rating + ' out of 5'}>
      {'★★★★★'.split('').map((s, i) => (
        <span key={i} className={i < Math.round(rating) ? 'star-on' : 'star-off'}>★</span>
      ))}
      <span className="rating-num">{rating.toFixed(1)}</span>
    </span>
  );
}

function TrustpilotBadge({ rating, reviews }) {
  // No real rating available -> show nothing rather than invent a score.
  if (rating == null || !isFinite(rating)) return null;
  const full = Math.round(rating);
  const title = 'Trustpilot ' + rating.toFixed(1) + ' / 5'
    + (reviews ? ' · ' + reviews.toLocaleString() + ' reviews' : '');
  return (
    <span className="tp-badge" title={title}>
      <span className="tp-logo"><span className="tp-logo-star">★</span>Trustpilot</span>
      <span className="tp-stars" aria-hidden="true">
        {[0, 1, 2, 3, 4].map(i => (
          <span key={i} className={'tp-sq' + (i < full ? ' on' : '')}>★</span>
        ))}
      </span>
      <span className="tp-score">{rating.toFixed(1)}</span>
    </span>
  );
}

// Collapsible AI name review — shows score header; click to expand benefits.
function NameReview({ domain, defaultOpen, compact, autoFetch }) {
  const [open, setOpen] = useState(!!defaultOpen);
  // When no review was pre-attached (e.g. a domain evaluated on the value page,
  // which has no suggest-flow review), the buy panel can fetch the SAME real AI
  // review on demand. Never fabricates — on failure it stays "Unavailable".
  const [fetched, setFetched] = useState(null);
  const [fetching, setFetching] = useState(false);
  // Whenever a review is already attached (suggest flow / favorites), seed the
  // shared cache so the SAME score is served if this domain is later evaluated
  // on the value page and bought there.
  useEffect(() => {
    if (domain.review && window.setReviewCache) {
      window.setReviewCache((domain.name + domain.ext).toLowerCase(), domain.review);
    }
  }, [domain.name, domain.ext, domain.review]);
  useEffect(() => {
    if (domain.review || !autoFetch) return;
    const full = (domain.name + domain.ext).toLowerCase();
    // Serve the previously-stored score for this domain if we have one — keeps
    // the buy panel consistent with the Names flow instead of re-rolling a new
    // non-deterministic AI score.
    const cached = window.getReviewCache && window.getReviewCache(full);
    if (cached) { setFetched(cached); setFetching(false); return; }
    let alive = true;
    setFetched(null); setFetching(true);
    fetch('/api/domains/review', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ domain: full }),
    })
      .then(async (resp) => {
        const data = await resp.json().catch(() => ({}));
        if (!alive) return;
        if (resp.ok && data && data.review) {
          setFetched(data.review);
          if (window.setReviewCache) window.setReviewCache(full, data.review);
        }
        setFetching(false);
      })
      .catch(() => { if (alive) setFetching(false); });
    return () => { alive = false; };
  }, [domain.name, domain.ext, autoFetch, domain.review]);
  const evalr = domain.review || fetched;
  if (!evalr) {
    return (
      <div className={'name-eval' + (compact ? ' name-eval-compact' : '')}>
        <div className="ne-head" aria-disabled="true">
          <span className="ne-title"><span className="ne-spark" aria-hidden="true">✦</span> AI name review</span>
          <span className="ne-score ne-score-muted">{fetching ? 'Scoring…' : 'Unavailable'}</span>
        </div>
      </div>
    );
  }
  return (
    <div className={'name-eval' + (compact ? ' name-eval-compact' : '')}>
      <button className="ne-head" onClick={() => setOpen(o => !o)} aria-expanded={open}>
        <span className="ne-title"><span className="ne-spark" aria-hidden="true">✦</span> AI name review</span>
        <span className="ne-score">
          <span className="ne-verdict">{evalr.verdict}</span>
          <span className="ne-num">· <strong>{evalr.score.toFixed(1)}</strong><span className="ne-of">/10</span></span>
          <span className={'ne-chev' + (open ? ' ne-chev-up' : '')} aria-hidden="true">
            <svg width="11" height="11" viewBox="0 0 12 12" fill="none"><path d="M2.5 4.25 6 7.75l3.5-3.5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>
          </span>
        </span>
      </button>
      {open ? (
        <div className="ne-list">
          {evalr.benefits.map((b, i) => (
            <div className={'ne-item' + (b.ok ? '' : ' ne-no')} key={i}>
              <span className="ne-mark" aria-hidden="true">{b.ok ? '✓' : '!'}</span>
              <span className="ne-text"><strong>{b.t}</strong><span className="ne-desc">{b.d}</span></span>
            </div>
          ))}
        </div>
      ) : null}
    </div>
  );
}

// Collapsible estimated domain value — powered by the real valuation
// (POST /api/domains/value). Cache-first so re-opening a panel is instant; never
// shows a fabricated number. On a provider/transport failure it honestly says
// the value is unavailable rather than inventing one.
function EstimatedValue({ domain }) {
  const fullDomain = (domain.name + domain.ext).toLowerCase();
  const [open, setOpen] = useState(false);
  const [state, setState] = useState(() => {
    const cached = window.getValueCache && window.getValueCache(fullDomain);
    return cached ? { status: 'ok', v: window.mapValuation(cached) } : { status: 'loading', v: null };
  });

  useEffect(() => {
    let alive = true;
    const cached = window.getValueCache && window.getValueCache(fullDomain);
    if (cached) { setState({ status: 'ok', v: window.mapValuation(cached) }); return; }
    setState({ status: 'loading', v: null });
    fetch('/api/domains/value', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ domain: fullDomain }),
    })
      .then(async (resp) => {
        const data = await resp.json().catch(() => ({}));
        if (!alive) return;
        if (!resp.ok || typeof data.mid !== 'number') { setState({ status: 'error', v: null }); return; }
        if (window.setValueCache) window.setValueCache(fullDomain, data);
        if (window.namitEnrich) window.namitEnrich(domain.id || fullDomain, { valuation: data });
        setState({ status: 'ok', v: window.mapValuation(data) });
      })
      .catch(() => { if (alive) setState({ status: 'error', v: null }); });
    return () => { alive = false; };
  }, [fullDomain]);

  const e = state.v;
  const f = (n) => '$' + Number(n).toLocaleString('en-US');
  const ok = state.status === 'ok' && e;
  const premiumExt = ok && (e.tldTier === 'premium' || e.tldTier === 'established');
  const head = state.status === 'loading'
    ? <span className="ne-score ne-score-muted">Estimating…</span>
    : !ok
      ? <span className="ne-score ne-score-muted">Unknown</span>
      : (
        <span className="ne-score"><strong>{f(e.val)}</strong> {e.currency}
          <span className={'ne-chev' + (open ? ' ne-chev-up' : '')} aria-hidden="true">
            <svg width="11" height="11" viewBox="0 0 12 12" fill="none"><path d="M2.5 4.25 6 7.75l3.5-3.5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>
          </span>
        </span>
      );
  return (
    <div className="name-eval">
      <button className="ne-head" onClick={() => ok && setOpen(o => !o)} aria-expanded={open} disabled={!ok}>
        <span className="ne-title"><span className="ne-spark" aria-hidden="true">$</span> Estimated value</span>
        {head}
      </button>
      {open && ok ? (
        <div className="ne-list">
          <div className="ne-item"><span className="ne-mark" aria-hidden="true">$</span><span className="ne-text"><strong>{f(e.val)} {e.currency}</strong><span className="ne-desc">Fair resale range {f(e.low)} – {f(e.high)}</span></span></div>
          {e.length != null ? (
            <div className="ne-item"><span className="ne-mark" aria-hidden="true">{e.length}</span><span className="ne-text"><strong>Length</strong><span className="ne-desc">{e.length} characters{e.syllableCount != null ? ', ' + e.syllableCount + ' syllable' + (e.syllableCount === 1 ? '' : 's') : ''} — {e.length <= 6 ? 'short and premium' : 'standard length'}</span></span></div>
          ) : null}
          {e.tldTier ? (
            <div className="ne-item"><span className="ne-mark" aria-hidden="true">{domain.ext.replace('.', '').slice(0, 2)}</span><span className="ne-text"><strong>Extension</strong><span className="ne-desc">{domain.ext} — {e.tldTier} tier, carries {premiumExt ? 'strong' : 'moderate'} resale demand</span></span></div>
          ) : null}
          {e.isBrandable != null ? (
            <div className="ne-item"><span className="ne-mark" aria-hidden="true">✦</span><span className="ne-text"><strong>Brandability</strong><span className="ne-desc">{e.isBrandable ? 'Rated brandable and ownable' : 'Limited brandability'}{e.mem != null ? ' · memorability ' + e.mem + '/100' : ''}</span></span></div>
          ) : null}
          {e.confidenceLabel ? (
            <p className="ne-conf">Estimate confidence: {e.confidenceLabel}. Algorithmic estimate of intrinsic domain value — a research guide, not an offer.</p>
          ) : null}
        </div>
      ) : null}
    </div>
  );
}

// Per-extension registrar pricing fetched from the backend (GET
// /api/domains/pricing), which proxies tldspy.com's daily-updated API. Cached
// per TLD for the session so re-opening panels for the same extension is
// instant. Every number here is real — registrars without a genuine price for
// the extension simply show no figure (never $0.00).
const pricingCache = {};

function formatMoney(amount, currency) {
  if (currency && currency !== 'USD') return currency + ' ' + amount.toFixed(2);
  return '$' + amount.toFixed(2);
}

function VendorBody({ domain }) {
  const baseOffers = vendorOffersFor(domain);
  const ext = (domain.ext || '').replace(/^\./, '').toLowerCase();

  const [pricing, setPricing] = React.useState(() => pricingCache[ext] || null);
  const [status, setStatus] = React.useState(() => (pricingCache[ext] ? 'done' : 'loading'));

  React.useEffect(() => {
    if (!ext) { setStatus('done'); return; }
    if (pricingCache[ext]) {
      setPricing(pricingCache[ext]);
      setStatus('done');
      return;
    }
    let alive = true;
    setStatus('loading');
    fetch('/api/domains/pricing?tld=' + encodeURIComponent(ext))
      .then((r) => (r.ok ? r.json() : Promise.reject(new Error('http ' + r.status))))
      .then((d) => {
        if (!alive) return;
        pricingCache[ext] = d;
        setPricing(d);
        setStatus('done');
      })
      .catch(() => { if (alive) setStatus('error'); });
    return () => { alive = false; };
  }, [ext]);

  const priceMap = (pricing && pricing.prices) || {};

  // Merge real prices in: registrars with a genuine register price are sorted
  // cheapest-first (the real "Best price"); the rest follow without a figure.
  const priced = [];
  const unpriced = [];
  baseOffers.forEach((v) => {
    const p = priceMap[v.id];
    if (p && typeof p.register === 'number') {
      priced.push({ ...v, register: p.register, renewal: p.renewal, currency: p.currency });
    } else {
      unpriced.push(v);
    }
  });
  priced.sort((a, b) => a.register - b.register);
  // Only flag a "Lowest price" when at least two registrars returned a real
  // price to compare. With partial coverage (e.g. the keyless demo feed) we
  // never imply a row beats registrars whose price is still unknown.
  if (priced.length >= 2) priced[0].bestPrice = true;
  const offers = priced.concat(unpriced);

  return (
    <React.Fragment>
      <div className="panel-head">
        <div>
          <div className="panel-kicker">Buy this domain</div>
          <div className="panel-domain">
            {domain.display.slice(0, domain.display.length - domain.ext.length)}
            <span className="domain-ext">{domain.ext}</span>
          </div>
        </div>
      </div>

      <NameReview domain={domain} autoFetch />

      <EstimatedValue domain={domain} />

      <p className="panel-sub panel-sub-tight">Compare live prices across registrars. You'll complete checkout on the registrar's site.</p>
      <div className="offers">
        {status === 'loading' ? baseOffers.map((v) => (
          <div className="offer offer-sk" key={v.id}>
            <div className="offer-left">
              <div className="sk sk-omono" />
              <div className="offer-info">
                <div className="sk sk-oname" />
                <div className="sk sk-orate" />
              </div>
            </div>
            <div className="offer-right">
              <div className="sk sk-oprice" />
              <div className="sk sk-obuy" />
            </div>
          </div>
        )) : offers.map((v) => (
          <div className={'offer' + (v.bestPrice ? ' offer-best' : '')} key={v.id}>
            <div className="offer-left">
              <div className="monogram" style={v.logo ? (v.fullBleed ? { background: v.bg } : { background: '#fff', border: '1px solid #ECEAE3', color: 'transparent' }) : { background: v.bg, color: v.fg, fontSize: v.mark.length > 1 ? '13px' : '17px' }}>
                {v.logo ? <img src={v.logo} alt={v.name + ' logo'} className={v.fullBleed ? 'vendor-logo vendor-logo-full' : 'vendor-logo'} /> : v.mark}
              </div>
              <div className="offer-info">
                <div className="offer-name">
                  {v.name}
                  {v.bestPrice ? <span className="tag tag-best">Lowest price</span> : null}
                  {v.perk ? <span className="tag">{v.perk}</span> : null}
                </div>
                <TrustpilotBadge rating={v.rating} reviews={v.reviews} />
              </div>
            </div>
            <div className="offer-right">
              {typeof v.register === 'number' ? (
                <React.Fragment>
                  <div className="offer-price">{formatMoney(v.register, v.currency)}<span className="per">/yr</span></div>
                  <div className="offer-renew">renews {formatMoney(v.renewal, v.currency)}</div>
                </React.Fragment>
              ) : (
                <div className="offer-renew offer-noprice">
                  {status === 'loading' ? 'Loading price…' : 'Price on registrar'}
                </div>
              )}
              <button
                className={'offer-buy' + (v.bestPrice ? ' offer-buy-best' : '')}
                onClick={() => {
                  // Registrar conversion: record WHICH provider was clicked.
                  if (window.namitRecordClick) {
                    window.namitRecordClick({ kind: 'registrar', clientId: domain.id, name: domain.name, ext: domain.ext, vendor: v.name });
                  }
                  window.open(registrarUrl(v, domain.name + domain.ext), '_blank', 'noopener,noreferrer');
                }}
                title={'Open ' + v.name + ' to register ' + domain.name + domain.ext}
              >Buy</button>
            </div>
          </div>
        ))}
      </div>
      <p className="panel-foot">Some vendors might have different offers or discounts depending on the term and period you register for, so the exact total is confirmed at the registrar's checkout.</p>
    </React.Fragment>
  );
}

// One persistent dock — stays mounted while either mode is active, so the page
// width never re-collapses. Only the inner content swaps (with a soft fade).
function GenerateHandlesPrompt({ domain, cost, canAfford, onGenerate, errored }) {
  return (
    <React.Fragment>
      <div className="panel-head">
        <div>
          <div className="panel-kicker">Social handles</div>
          <div className="panel-domain">@{domain.name}</div>
        </div>
      </div>
      <div className="gh-prompt">
        <div className="gh-icon" aria-hidden="true">@</div>
        <div className="gh-title">{errored ? 'That check didn’t go through' : 'Handles not checked yet'}</div>
        {errored
          ? <p className="gh-text">We couldn’t reach the handle service for <strong>{domain.display}</strong>, so <strong>your credit wasn’t used</strong>. You can try again.</p>
          : <p className="gh-text">You didn't run a social handle check for <strong>{domain.display}</strong>. Check availability across all 14 platforms now.</p>}
        <button className="btn-primary gh-btn" onClick={onGenerate} disabled={!canAfford}>
          {errored ? 'Try again · ' : 'Generate handles · '}{cost} credit{cost > 1 ? 's' : ''}
        </button>
        {!canAfford ? <p className="gh-note">Not enough credits — top up to continue.</p> : null}
      </div>
    </React.Fragment>
  );
}

function SidePanel({ panel, onClose, handlesGenerated, onGenerateHandles, handleCost, canAffordHandles, onHandlesOutcome, handleErrored, availChecked, onCheckAvail, availCost, canAffordAvail, onAvailOutcome, availErrored }) {
  const [closing, setClosing] = useState(false);
  const close = () => { setClosing(true); setTimeout(() => { setClosing(false); onClose(); }, 260); };
  useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape') close(); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, []);
  useEffect(() => {
    if (!panel) return;
    if (!window.matchMedia('(max-width: 560px)').matches) return;
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    return () => { document.body.style.overflow = prev; };
  }, [panel]);
  if (!panel) return null;
  const { mode, domain } = panel;
  const label = mode === 'buy' ? 'Choose a registrar' : mode === 'avail' ? 'Domain availability' : 'Social handle availability';
  const innerKey = mode + ':' + domain.id + ':' + ((mode === 'avail' || mode === 'buy') ? (availChecked ? 'c' : 'u') : (handlesGenerated ? 'g' : 'n'));
  return (
    <div className={'panel-root' + (closing ? ' panel-closing' : '')}>
      <div className="scrim" onClick={close}></div>
      <aside className="panel" role="dialog" aria-label={label}>
        <button className="btn-close panel-close-abs" onClick={close} aria-label="Close">×</button>
        <div className="panel-inner" key={innerKey}>
          {mode === 'buy'
            ? (domain.available == null
                ? (availChecked
                    ? <AvailBody domain={domain} onOutcome={onAvailOutcome} />
                    : <CheckAvailPrompt domain={domain} cost={availCost} canAfford={canAffordAvail} errored={availErrored} onCheck={() => onCheckAvail(domain)} />)
                : <VendorBody domain={domain} />)
            : mode === 'avail'
              ? (availChecked
                  ? <AvailBody domain={domain} onOutcome={onAvailOutcome} />
                  : <CheckAvailPrompt domain={domain} cost={availCost} canAfford={canAffordAvail} errored={availErrored} onCheck={() => onCheckAvail(domain)} />)
              : handlesGenerated
                ? <HandlesBody domain={domain} onOutcome={onHandlesOutcome} />
                : <GenerateHandlesPrompt domain={domain} cost={handleCost} canAfford={canAffordHandles} errored={handleErrored} onGenerate={() => onGenerateHandles(domain)} />}
        </div>
      </aside>
    </div>
  );
}

Object.assign(window, { PromptBox, TldChips, SkeletonRows, ResultRow, SidePanel, NameReview });
