/* src/v2/watchlist.jsx — WatchlistDrawer.
   Reads/writes 'tmt_watchlist' (shared with watchlist-sidebar.js, bulletin.js,
   account.html) as full records: { symbol, name, type, yahooSymbol, addedAt }.
   Older string-only entries are normalized on read and re-saved on next
   mutation. Live prices come from /batch-quotes; rows render '—' until the
   first poll resolves so the UI never shows fake zeros. */

const { useState: _wlS, useEffect: _wlE, useMemo: _wlM } = React;

const WL_KEY = (window.TMT_API && window.TMT_API.WATCHLIST_KEY) || 'tmt_watchlist';
const QUOTE_REFRESH_MS = 30 * 1000;

// Bond ETFs and Treasury yield indices treated as Fixed Income for the
// categorized view. Anything else falls through to type/symbol heuristics.
const FIXED_INCOME_SYMBOLS = new Set([
  'TLT','IEF','SHY','BND','AGG','LQD','HYG','JNK','MUB','VCIT','VCSH','VGSH','VGIT','VGLT',
  'EMB','TIP','SCHO','SCHR','SCHQ','BIL','GOVT','MBB','SHV','BSV','BIV','BLV','BNDX','VWOB',
  '^TNX','^TYX','^FVX','^IRX'
]);

function inferType(symbol) {
  const s = String(symbol || '').toUpperCase();
  if (!s) return 'OTHER';
  if (FIXED_INCOME_SYMBOLS.has(s)) return 'FIXED_INCOME';
  if (s.startsWith('^')) return 'INDEX';
  if (s.endsWith('-USD') || s.endsWith('-USDT')) return 'CRYPTO';
  if (s.endsWith('=F')) return 'FUTURE';
  if (s.endsWith('=X')) return 'CURRENCY';
  return 'EQUITY';
}

function normalizeEntry(entry) {
  if (!entry) return null;
  if (typeof entry === 'string') {
    const sym = entry.toUpperCase();
    if (!sym) return null;
    return { symbol: sym, name: sym, type: inferType(sym), yahooSymbol: sym };
  }
  const symbol = String(entry.symbol || entry.sym || '').toUpperCase();
  if (!symbol) return null;
  return {
    symbol,
    name: entry.name || entry.shortName || symbol,
    type: String(entry.type || inferType(symbol)).toUpperCase(),
    yahooSymbol: entry.yahooSymbol || symbol,
    addedAt: entry.addedAt,
  };
}

function loadWatchlist() {
  try {
    const raw = localStorage.getItem(WL_KEY);
    if (!raw) return [];
    const parsed = JSON.parse(raw);
    if (!Array.isArray(parsed)) return [];
    return parsed.map(normalizeEntry).filter(Boolean);
  } catch { return []; }
}

function saveWatchlist(items) {
  try { localStorage.setItem(WL_KEY, JSON.stringify(items)); } catch {}
}

function categorize(item) {
  const sym = String(item.symbol || '').toUpperCase();
  const t = String(item.type || '').toUpperCase();
  if (FIXED_INCOME_SYMBOLS.has(sym) || t === 'FIXED_INCOME' || t === 'BOND') return 'FIXED_INCOME';
  if (t.includes('CRYPTO')) return 'CRYPTO';
  if (t.includes('INDEX') || sym.startsWith('^')) return 'INDEX';
  if (t === 'ETF' || t === 'MUTUALFUND') return 'ETF';
  if (t.includes('FUTURE') || sym.endsWith('=F')) return 'FUTURE';
  if (t.includes('CURRENCY') || sym.endsWith('=X')) return 'CURRENCY';
  if (t === 'EQUITY' || t === 'STOCK') return 'EQUITY';
  return 'OTHER';
}

const CATEGORY_ORDER = ['EQUITY', 'ETF', 'INDEX', 'CRYPTO', 'FIXED_INCOME', 'FUTURE', 'CURRENCY', 'OTHER'];
const CATEGORY_LABELS = {
  EQUITY: 'Equities',
  ETF: 'ETFs',
  INDEX: 'Indices',
  CRYPTO: 'Crypto',
  FIXED_INCOME: 'Fixed Income',
  FUTURE: 'Futures',
  CURRENCY: 'FX',
  OTHER: 'Other',
};

/**
 * useWatchlistQuotes — fetch /batch-quotes for the items' yahooSymbols and
 * map results back to the watchlist's canonical symbol. Returns {SYMBOL: row}
 * where row is { px, ch, chPct, name }. Empty until first poll resolves so
 * callers can render '—' placeholders instead of fake zeros.
 */
function useWatchlistQuotes(items) {
  const [quotes, setQuotes] = _wlS({});
  // Yahoo wants dashes for class shares (BRK-B, BF-B). Older watchlist
  // entries stored as "BRK.B" silently 404 against /batch-quotes — normalize
  // to the dash form on the wire and let matchItem reverse it on render.
  const wireSymbols = items.map(i => String(i.yahooSymbol || i.symbol).replace(/\./g, '-'));
  const symKey = wireSymbols.join(',');

  _wlE(() => {
    if (!window.TMT_API || items.length === 0) {
      setQuotes({});
      return;
    }
    let aborted = false;

    // Vercel cold starts often return 503 or time out on the first call after
    // the function has been idle. A single failed poll then leaves the UI on
    // '—' for the full 30s interval. Retry the first poll with backoff so
    // cold-start latency doesn't read as "broken".
    const fetchOnce = () => window.TMT_API.getJSON(
      '/batch-quotes?symbols=' + encodeURIComponent(symKey)
    );
    const fetchWithRetry = async () => {
      let lastErr;
      for (let attempt = 0; attempt < 3; attempt++) {
        try { return await fetchOnce(); }
        catch (e) {
          lastErr = e;
          if (aborted || attempt === 2) break;
          await new Promise(r => setTimeout(r, 1500 * (attempt + 1)));
        }
      }
      throw lastErr;
    };

    // Match an API quote back to the watchlist row it belongs to. Yahoo
    // canonicalizes some symbols (e.g. "BRK.B" → "BRK-B"), so also try a
    // dot/dash-normalized comparison before giving up.
    const norm = s => String(s || '').replace(/\./g, '-').toUpperCase();
    const matchItem = (q) => items.find(i =>
      (i.yahooSymbol && i.yahooSymbol === q.symbol) ||
      i.symbol === q.symbol ||
      norm(i.yahooSymbol || i.symbol) === norm(q.symbol)
    ) || null;

    const poll = async () => {
      try {
        const data = await fetchWithRetry();
        if (aborted) return;
        const list = (data && (data.quotes || data.data)) || [];
        const next = {};
        list.forEach(q => {
          if (!q || !q.symbol) return;
          const matched = matchItem(q);
          const key = matched ? matched.symbol : q.symbol;
          const px = q.regularMarketPrice != null ? q.regularMarketPrice : q.price;
          if (px == null) return;
          const ch = q.change != null
            ? q.change
            : (q.regularMarketChange != null ? q.regularMarketChange : 0);
          const chPct = q.changePercent != null
            ? q.changePercent
            : (q.regularMarketChangePercent != null ? q.regularMarketChangePercent : 0);
          next[key] = {
            px,
            ch,
            chPct,
            name: q.shortName || q.longName || (matched && matched.name) || key,
          };
        });
        setQuotes(next);
      } catch (e) {
        // Keep last good values; don't blank the UI on a single failed poll.
        console.debug('[watchlist] poll failed', e && e.message);
      }
    };

    poll();
    const id = setInterval(poll, QUOTE_REFRESH_MS);
    return () => { aborted = true; clearInterval(id); };
  }, [symKey]);

  return quotes;
}

function fmtChg(n) {
  if (n == null || isNaN(n)) return '—';
  return (n >= 0 ? '+' : '') + Number(n).toFixed(2);
}

function WatchlistDrawer({ open, onClose }) {
  const [items, setItems] = _wlS(loadWatchlist);
  const [query, setQuery] = _wlS('');
  const [searchResults, setSearchResults] = _wlS([]);
  const [searching, setSearching] = _wlS(false);

  // Re-read storage when other surfaces (search bar, sidebar) update the list
  _wlE(() => {
    const reload = () => setItems(loadWatchlist());
    const onStorage = (e) => { if (e.key === WL_KEY) reload(); };
    window.addEventListener('watchlistUpdated', reload);
    window.addEventListener('storage', onStorage);
    return () => {
      window.removeEventListener('watchlistUpdated', reload);
      window.removeEventListener('storage', onStorage);
    };
  }, []);

  const quotes = useWatchlistQuotes(items);

  // Hot search — hits /search endpoint, debounced
  _wlE(() => {
    if (!query || query.length < 1) { setSearchResults([]); return; }
    const q = query.trim();
    if (!q) { setSearchResults([]); return; }
    let alive = true;
    setSearching(true);
    const id = setTimeout(async () => {
      try {
        const data = await window.TMT_API.getJSON('/search?q=' + encodeURIComponent(q));
        if (!alive) return;
        setSearchResults((data && data.results) || []);
      } catch { if (alive) setSearchResults([]); }
      finally { if (alive) setSearching(false); }
    }, 220);
    return () => { alive = false; clearTimeout(id); };
  }, [query]);

  const addEntry = (entry) => {
    const normalized = normalizeEntry(
      typeof entry === 'string'
        ? entry
        : {
            symbol: entry.symbol,
            name: entry.shortname || entry.longname || entry.name,
            type: entry.type,
            yahooSymbol: entry.symbol,
          }
    );
    if (!normalized) return;
    if (items.some(i => i.symbol === normalized.symbol)) return;
    const next = [...items, { ...normalized, addedAt: new Date().toISOString() }];
    setItems(next);
    saveWatchlist(next);
    setQuery('');
    setSearchResults([]);
    window.dispatchEvent(new Event('watchlistUpdated'));
  };

  const removeSymbol = (sym) => {
    const next = items.filter(i => i.symbol !== sym);
    setItems(next);
    saveWatchlist(next);
    window.dispatchEvent(new Event('watchlistUpdated'));
  };

  const exportCsv = () => {
    const header = 'symbol,name,type,price,change,change_pct\n';
    const body = items.map(i => {
      const r = quotes[i.symbol] || {};
      return [i.symbol, (i.name || '').replace(/,/g, ' '), i.type || '', r.px ?? '', r.ch ?? '', r.chPct ?? ''].join(',');
    }).join('\n');
    const blob = new Blob([header + body], { type: 'text/csv' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url; a.download = 'watchlist.csv';
    a.click();
    URL.revokeObjectURL(url);
  };

  const grouped = _wlM(() => {
    const buckets = {};
    items.forEach(item => {
      const cat = categorize(item);
      if (!buckets[cat]) buckets[cat] = [];
      buckets[cat].push(item);
    });
    return buckets;
  }, [items]);

  if (!open) return null;

  return (
    <>
      <div className="scrim" onClick={onClose} />
      <aside className="drawer" role="dialog" aria-label="Watchlist">
        <div className="drawer-hd">
          <div>
            <h3>Watchlist</h3>
            <div className="sub">{items.length} symbols · stored locally</div>
          </div>
          <button className="x" onClick={onClose} aria-label="Close">✕</button>
        </div>
        <div className="drawer-search">
          <input
            type="text"
            placeholder="Add symbol or search…"
            value={query}
            onChange={e => setQuery(e.target.value.toUpperCase())}
            onKeyDown={e => { if (e.key === 'Enter' && query) addEntry(query); }}
            autoFocus
          />
          <button className="tb-btn" onClick={exportCsv} title="Export CSV" style={{ fontSize: 11 }}>Export</button>
        </div>
        {query && (
          <div style={{ borderBottom: '1px solid var(--rule-soft)', maxHeight: 240, overflow: 'auto' }}>
            {searching && <div style={{ padding: 12, color: 'var(--ink-3)', fontStyle: 'italic', fontFamily: 'var(--serif)' }}>Searching…</div>}
            {!searching && searchResults.length === 0 && (
              <div style={{ padding: 12, color: 'var(--ink-3)', fontSize: 12 }}>
                Press Enter to add <b style={{ fontFamily: 'var(--mono)', color: 'var(--ink)' }}>{query}</b>
              </div>
            )}
            {!searching && searchResults.slice(0, 8).map((r, i) => (
              <button
                key={i}
                onClick={() => addEntry(r)}
                style={{
                  display: 'flex', alignItems: 'center', gap: 10, width: '100%',
                  padding: '9px 20px', cursor: 'pointer', borderBottom: '1px solid var(--rule-soft)',
                  textAlign: 'left',
                }}
              >
                <span style={{ fontFamily: 'var(--mono)', fontWeight: 700, color: 'var(--ink)', minWidth: 60 }}>{r.symbol}</span>
                <span style={{ flex: 1, color: 'var(--ink-2)', fontSize: 12, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                  {r.shortname || r.longname || r.name || ''}
                </span>
                <span style={{ color: 'var(--ink-4)', fontSize: 10 }}>{r.exchange || ''}</span>
              </button>
            ))}
          </div>
        )}
        <div className="wl-thead">
          <span>Symbol</span>
          <span className="num">Price</span>
          <span className="num">Chg</span>
          <span className="num">%</span>
        </div>
        <div className="wl-body">
          {items.length === 0 && (
            <div style={{ padding: 24, color: 'var(--ink-3)', fontFamily: 'var(--serif)', fontStyle: 'italic' }}>
              Empty — search above to add symbols.
            </div>
          )}
          {CATEGORY_ORDER.map(cat => {
            const list = grouped[cat];
            if (!list || list.length === 0) return null;
            return (
              <div key={cat}>
                <div className="wl-cat-hd">
                  <span className="wl-cat-name">{CATEGORY_LABELS[cat]}</span>
                  <span className="wl-cat-count">{list.length}</span>
                </div>
                {list.map(item => {
                  const sym = item.symbol;
                  const row = quotes[sym];
                  const px = row ? row.px : null;
                  const ch = row ? row.ch : null;
                  const chPct = row ? row.chPct : null;
                  const hasData = chPct != null;
                  const up = hasData ? chPct >= 0 : false;
                  return (
                    <a
                      key={sym}
                      className="wl-row"
                      href={'/details?symbol=' + encodeURIComponent(sym) + '&yahoo=' + encodeURIComponent(item.yahooSymbol || sym)}
                      style={{ textDecoration: 'none' }}
                    >
                      <div className="sym-cell">
                        <span className="lbl">{sym}</span>
                        <span className="co">{(row && row.name) || item.name || ''}</span>
                      </div>
                      <span className="num">{fmtPx(px)}</span>
                      <span className="num" style={{ color: hasData ? (up ? 'var(--up)' : 'var(--down)') : 'var(--ink-3)' }}>
                        {fmtChg(ch)}
                      </span>
                      <span className={'pct ' + (hasData ? (up ? 'up' : 'down') : '')}>{fmtPct(chPct)}</span>
                      <button
                        onClick={(e) => { e.preventDefault(); e.stopPropagation(); removeSymbol(sym); }}
                        title="Remove"
                        style={{
                          position: 'absolute', right: 4, top: '50%', transform: 'translateY(-50%)',
                          width: 20, height: 20, color: 'var(--ink-4)', fontSize: 14, opacity: 0,
                          transition: 'opacity 140ms',
                        }}
                        onMouseEnter={e => e.currentTarget.style.opacity = 1}
                        onMouseLeave={e => e.currentTarget.style.opacity = 0}
                      >×</button>
                    </a>
                  );
                })}
              </div>
            );
          })}
        </div>
      </aside>
    </>
  );
}

Object.assign(window, { WatchlistDrawer });
