// Site-wide search palette.
//
// The catalog is small enough (~30 releases, ~300 tracklist entries, ~150
// podcast chapters, a couple-dozen events) that everything fits in memory
// and we can scan on every keystroke without any indexer or fuzzy library.
// Built into a flat list of entries, each shaped:
//     { kind, label, sub, target: [routeName, optionalId], _kw }
// where _kw is a pre-lowercased "everything searchable" string.
//
// Trigger lives in the top bar; the palette also opens on ⌘K / Ctrl-K / /.
// ────────────────────────────────────────────────────────────────────────

const {
  RELEASES, RELEASE_DETAILS, CHAPTERS, EVENTS,
  splitArtists, canonicalArtist, releaseContributors,
  isLinkableArtist, slugifyArtist, artistReleases, artistChapters, aliasesOf,
  allChapters, normalizeChapterArtist, chapterHeading,
} = window.MOTD_DATA;

// ────────────────────────────────────────────────────────────────────────
// Index
// ────────────────────────────────────────────────────────────────────────
function buildSearchIndex() {
  const entries = [];

  // Releases — title, artist, cat number, format, released date all
  // searchable; result navigates to the detail page.
  for (const r of RELEASES) {
    entries.push({
      kind: 'Release',
      label: r.title,
      sub: `${r.artist} \u00b7 ${r.cat}${r.released ? ' \u00b7 ' + r.released : ''}`,
      keywords: [r.title, r.artist, r.cat, r.format, r.released].filter(Boolean).join(' '),
      target: ['release', r.id],
    });
  }

  // Artists — one entry per canonical name with 2+ appearances across
  // releases + podcast chapters (matches the in-page link affordance).
  // Aliases are folded into the keywords so typing "Pavaka" still surfaces
  // William Selman, "The Spiral" finds Oliver Chapoy, etc.
  const seen = new Set();
  for (const r of RELEASES) {
    for (const a of releaseContributors(r)) {
      if (!isLinkableArtist(a) || seen.has(a)) continue;
      seen.add(a);
      const rs = artistReleases(a);
      const chs = artistChapters(a);
      if (rs.length < 1 || rs.length + chs.length < 2) continue;
      const aliases = aliasesOf(a);
      const bits = [];
      if (rs.length)  bits.push(`${rs.length} release${rs.length === 1 ? '' : 's'}`);
      if (chs.length) bits.push(`${chs.length} podcast${chs.length === 1 ? '' : 's'}`);
      const sub = aliases.length > 0
        ? `${bits.join(' \u00b7 ')} \u00b7 also as ${aliases.join(', ')}`
        : bits.join(' \u00b7 ');
      entries.push({
        kind: 'Artist',
        label: a,
        sub,
        keywords: [a, ...aliases].join(' '),
        target: ['artist', slugifyArtist(a)],
      });
    }
  }

  // Tracks — every titled tracklist row across every release with detail
  // data.  Sub is the parent release so the user knows where the track
  // lives; clicking jumps to that release's page.
  for (const r of RELEASES) {
    const detail = RELEASE_DETAILS[r.id];
    if (!detail) continue;
    const all = [...(detail.sideA || []), ...(detail.sideB || [])];
    for (const row of all) {
      const title = row[1];
      if (!title) continue;
      entries.push({
        kind: 'Track',
        label: title,
        sub: `${r.title} \u00b7 ${r.artist} \u00b7 ${r.cat}`,
        keywords: `${title} ${r.title} ${r.artist} ${r.cat}`,
        target: ['release', r.id],
      });
    }
  }

  // Podcast chapters.  Clicking jumps to /mixes — we don't have per-chapter
  // landing pages, but the chapter index page scrolls there.  Read from
  // `allChapters()` so once the live SoundCloud feed loads we index its
  // 150+ entries (rather than the static 30-entry fallback).  Both the
  // label and sub use `chapterHeading()` so live feed entries that arrive
  // formatted as "ARTIST | MIX TITLE" render in title case here too.
  for (const c of (allChapters() || [])) {
    const heading = chapterHeading(c);
    const cleanArtist = normalizeChapterArtist(c.artist) || c.artist;
    entries.push({
      kind: 'Podcast',
      label: `Chapter ${c.num} \u00b7 ${heading}`,
      sub: `${cleanArtist}${c.where ? ' \u00b7 ' + c.where : ''}${c.date ? ' \u00b7 ' + c.date : ''}`,
      keywords: `${c.title || ''} ${c.artist || ''} ${heading} ${cleanArtist} ${c.where || ''} chapter ${c.num}`,
      // Deep-link to the specific row on the mixes page.  MixesPage reads
      // the anchor and scrolls + flashes the matching `chapter-{num}`
      // element.  Clicking the row itself still opens the SC upload.
      target: ['mixes', null, `chapter-${c.num}`],
    });
  }

  // Events.  Deep-link to a specific card (date+name slug) on the events
  // page so the visitor lands directly on the row they searched for.
  const eventAnchorId = window.MOTD_ANCHORS && window.MOTD_ANCHORS.eventAnchorId;
  for (const e of EVENTS || []) {
    entries.push({
      kind: 'Event',
      label: e.name,
      sub: `${e.venue || ''}${e.city ? ', ' + e.city : ''}${e.date ? ' \u00b7 ' + e.date : ''}${e.lineup ? ' \u00b7 ' + e.lineup : ''}`,
      keywords: `${e.name || ''} ${e.venue || ''} ${e.city || ''} ${e.lineup || ''} ${e.date || ''}`,
      target: ['events', null, eventAnchorId ? eventAnchorId(e) : null],
    });
  }

  return entries.map(e => ({
    ...e,
    _label: e.label.toLowerCase(),
    _kw: (e.label + ' ' + e.keywords).toLowerCase(),
  }));
}

let _searchIndex = null;
const getSearchIndex = () => (_searchIndex ||= buildSearchIndex());

// Invalidate the cached index when the live SoundCloud feed loads (or any
// other source bumps the catalog).  Cheap to rebuild — ~few hundred entries.
if (typeof window !== 'undefined') {
  window.addEventListener('motd-live-chapters', () => { _searchIndex = null; });
}

// Score a single entry for query `q` (already lowercased).
// Higher = better match.  Negative weight by entry.kind preserves the
// section ordering (Releases before Artists before Tracks ...).
const KIND_ORDER  = ['Release', 'Artist', 'Track', 'Podcast', 'Event'];
const KIND_BONUS  = { Release: 5, Artist: 4, Track: 2, Podcast: 1, Event: 1 };

function scoreEntry(entry, q) {
  const label = entry._label;
  let score = 0;
  if (label.startsWith(q))                                        score = 100;
  else if (new RegExp('\\b' + q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).test(label)) score = 80;
  else if (label.includes(q))                                     score = 60;
  else if (entry._kw.includes(q))                                 score = 30;
  if (!score) return 0;
  return score + (KIND_BONUS[entry.kind] || 0);
}

const PER_GROUP = 6;

function runSearch(rawQ) {
  const q = rawQ.trim().toLowerCase();
  if (!q) return { groups: {}, flat: [] };
  const idx = getSearchIndex();
  const scored = [];
  for (const e of idx) {
    const s = scoreEntry(e, q);
    if (s > 0) scored.push([s, e]);
  }
  scored.sort((a, b) => b[0] - a[0]);
  const groups = {};
  for (const [, e] of scored) {
    (groups[e.kind] ||= []).push(e);
  }
  // Cap each group, then flatten in section order for keyboard nav.
  const flat = [];
  for (const k of KIND_ORDER) {
    if (groups[k]) {
      groups[k] = groups[k].slice(0, PER_GROUP);
      for (const e of groups[k]) flat.push(e);
    }
  }
  return { groups, flat };
}

// ────────────────────────────────────────────────────────────────────────
// Trigger (top bar)
// ────────────────────────────────────────────────────────────────────────
// Detect mac for accurate keyboard hint glyph.  Falls back to Ctrl on
// Windows / Linux.
const IS_MAC = typeof navigator !== 'undefined'
  && /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent || '');

// Trigger lives inside the top-bar `.nav-links` list as a plain text item
// styled identically to "Latest / Label / Podcast / …" so the whole nav
// reads as a single typographic row, no icon orphan.  Mouse + keyboard
// behavior is unchanged — onOpen wires up the palette.
const SearchTrigger = ({ onOpen }) => (
  <span
    role="button"
    tabIndex={0}
    onClick={onOpen}
    onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onOpen(); } }}
    aria-label="Search"
    title="Search"
  >Search</span>
);

// ────────────────────────────────────────────────────────────────────────
// Palette
// ────────────────────────────────────────────────────────────────────────
const KIND_TITLE = {
  Release: 'Releases',
  Artist:  'Artists',
  Track:   'Tracks',
  Podcast: 'Podcast',
  Event:   'Events',
};

const SearchPalette = ({ open, onClose, go }) => {
  const [q, setQ] = React.useState('');
  const [sel, setSel] = React.useState(0);
  const inputRef = React.useRef(null);
  const listRef  = React.useRef(null);

  // Focus the input on open; clear state on close.
  React.useEffect(() => {
    if (open) {
      setQ('');
      setSel(0);
      // requestAnimationFrame so focus runs after the element is in the
      // tree (React renders, then we focus).
      requestAnimationFrame(() => inputRef.current && inputRef.current.focus());
    }
  }, [open]);

  // Lock body scroll while palette is open.
  React.useEffect(() => {
    if (!open) return;
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    return () => { document.body.style.overflow = prev; };
  }, [open]);

  const { groups, flat } = React.useMemo(() => runSearch(q), [q]);

  // Clamp selected index when results shrink/grow.
  React.useEffect(() => {
    if (sel >= flat.length) setSel(Math.max(0, flat.length - 1));
  }, [flat.length]);

  // Keep the selected row visible inside the scrolling results column.
  React.useEffect(() => {
    if (!open || !listRef.current) return;
    const row = listRef.current.querySelector('.search-result.sel');
    if (!row) return;
    const list = listRef.current;
    const rTop = row.offsetTop;
    const rBot = rTop + row.offsetHeight;
    if (rTop < list.scrollTop) list.scrollTop = rTop - 8;
    else if (rBot > list.scrollTop + list.clientHeight) list.scrollTop = rBot - list.clientHeight + 8;
  }, [sel, open]);

  const choose = (entry) => {
    if (!entry) return;
    go(...entry.target);
    onClose();
  };

  const onKey = (e) => {
    if (e.key === 'Escape')          { e.preventDefault(); onClose(); }
    else if (e.key === 'ArrowDown')  { e.preventDefault(); setSel(i => Math.min(flat.length - 1, i + 1)); }
    else if (e.key === 'ArrowUp')    { e.preventDefault(); setSel(i => Math.max(0, i - 1)); }
    else if (e.key === 'Enter')      { e.preventDefault(); choose(flat[sel]); }
  };

  if (!open) return null;

  return (
    <div className="search-overlay" onMouseDown={onClose}>
      <div className="search-panel" onMouseDown={(e) => e.stopPropagation()}>
        <div className="search-input-row">
          <span className="search-input-glyph" aria-hidden="true">
            <svg width="16" height="16" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.2">
              <circle cx="6" cy="6" r="4.5" />
              <line x1="9.4" y1="9.4" x2="12.6" y2="12.6" strokeLinecap="round" />
            </svg>
          </span>
          <input
            ref={inputRef}
            value={q}
            onChange={(e) => { setQ(e.target.value); setSel(0); }}
            onKeyDown={onKey}
            placeholder=""
            className="search-input"
            spellCheck={false}
            autoComplete="off"
          />
          <button className="search-close" onClick={onClose} type="button">esc</button>
        </div>

        <div className="search-results" ref={listRef}>
          {q.trim() === '' && (
            <div className="search-hint">
              <p className="search-hint-body">
                Search label releases, podcasts, artists, or events.
              </p>
            </div>
          )}

          {q.trim() !== '' && flat.length === 0 && (
            <div className="search-hint">
              <p className="search-hint-lead">No matches for &ldquo;{q}&rdquo;.</p>
              <p className="search-hint-body">
                Try a partial word — searches everything in the catalog at once.
              </p>
            </div>
          )}

          {KIND_ORDER.map(kind => {
            const list = groups[kind];
            if (!list || list.length === 0) return null;
            return (
              <div className="search-group" key={kind}>
                <div className="search-group-label">{KIND_TITLE[kind]}</div>
                {list.map((e) => {
                  const idx = flat.indexOf(e);
                  return (
                    <div
                      key={kind + ':' + idx + ':' + e.label}
                      className={`search-result ${sel === idx ? 'sel' : ''}`}
                      onMouseMove={() => setSel(idx)}
                      onClick={() => choose(e)}
                    >
                      <div className="search-result-label">{e.label}</div>
                      <div className="search-result-sub">{e.sub}</div>
                    </div>
                  );
                })}
              </div>
            );
          })}
        </div>

        <div className="search-footer">
          <span><kbd>↑</kbd><kbd>↓</kbd> Navigate</span>
          <span><kbd>⏎</kbd> Open</span>
          <span><kbd>esc</kbd> Close</span>
        </div>
      </div>
    </div>
  );
};

window.MOTD_SEARCH = { SearchTrigger, SearchPalette };
