{% comment %}
  SNIPPET version of the Dynasort predictive/live-search takeover (Chunk S2 #3).
  Use this when the section can't be placed globally (Horizon's header/footer are
  locked section groups). Put this file at snippets/dynasort-live-search.liquid and
  render it once from theme.liquid (just before </body>) so it loads on every page:

    {% render 'dynasort-live-search',
        input_selector: 'input[type="search"], input[name="q"]',
        suppress_selector: '' %}

  Params (all optional, sensible defaults):
    input_selector     CSS selector for the theme's search field(s)
    suppress_selector  CSS selector of the theme's native predictive dropdown to hide
    min_chars          minimum chars before querying (default 2)
    max_results        suggestions to show (default 6)
    debounce_ms        keystroke debounce (default 250)

  It attaches to the search input(s) and, on each debounced keystroke, calls the App
  Proxy in predictive mode and renders OUR recipe-reordered suggestions. Enter /
  submit still posts to /search → the results-page section takes over.
{% endcomment %}

{% liquid
  assign ds_input = block.settings.input_selector | default: 'input[type="search"], input[name="q"]'
  assign ds_suppress = block.settings.suppress_selector | default: ''
  assign ds_min = block.settings.min_chars | default: 2
  assign ds_limit = block.settings.max_results | default: 6
  assign ds_debounce = block.settings.debounce_ms | default: 250
%}

<div
  data-dynasort-predict
  data-input-selector="{{ ds_input | escape }}"
  data-suppress-selector="{{ ds_suppress | escape }}"
  data-min-chars="{{ ds_min }}"
  data-limit="{{ ds_limit }}"
  data-debounce="{{ ds_debounce }}"
  hidden
></div>

<style>
  .dynasort-predict-panel {
    position: fixed; z-index: 9999; color: #1a1a1a;
    background: #ffffff;
    border: 1px solid color-mix(in srgb, currentColor 16%, transparent);
    border-radius: var(--media-radius, 8px); box-shadow: 0 8px 28px rgba(0,0,0,0.14);
    overflow: hidden; font-size: 14px; max-height: 70vh; overflow-y: auto;
  }
  .dynasort-predict-panel[hidden] { display: none; }
  /* In-flow mode: inside a search modal (Horizon family) the panel joins the
     modal's own layout instead of floating, so its geometry can never fight
     the dialog's transforms, top layer, or overflow. */
  .dynasort-predict-panel--inflow { position: static; box-shadow: none; border: 0; border-radius: 0; max-height: none; }
  .dynasort-predict-item { display: flex; align-items: center; gap: 0.75em; padding: 0.55em 0.8em; text-decoration: none; color: inherit; }
  .dynasort-predict-item:hover, .dynasort-predict-item.is-active { background: color-mix(in srgb, currentColor 7%, transparent); }
  .dynasort-predict-thumb { width: 44px; height: 44px; flex: 0 0 auto; object-fit: cover; border-radius: 6px; background: color-mix(in srgb, currentColor 12%, transparent); }
  .dynasort-predict-meta { min-width: 0; }
  .dynasort-predict-title { display: block; line-height: 1.3; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .dynasort-predict-price { font-size: 0.88em; color: color-mix(in srgb, currentColor 60%, transparent); }
  .dynasort-predict-price del { margin-right: 0.35em; }
  .dynasort-predict-all { display: block; padding: 0.6em 0.8em; text-align: center; font-size: 0.9em; text-decoration: none; color: inherit; border-top: 1px solid color-mix(in srgb, currentColor 12%, transparent); }
  .dynasort-predict-empty { padding: 0.8em; color: color-mix(in srgb, currentColor 60%, transparent); }
</style>

<script>
(function () {
  var cfg = document.currentScript.closest('[data-dynasort-predict]') || document.querySelector('[data-dynasort-predict]');
  if (!cfg || window.__dynasortPredictInit) return;
  window.__dynasortPredictInit = true;

  var inputSelector = cfg.getAttribute('data-input-selector') || 'input[type="search"], input[name="q"]';
  var suppressSelector = (cfg.getAttribute('data-suppress-selector') || '').trim();
  var minChars = parseInt(cfg.getAttribute('data-min-chars'), 10) || 2;
  var limit = parseInt(cfg.getAttribute('data-limit'), 10) || 6;
  var debounceMs = parseInt(cfg.getAttribute('data-debounce'), 10) || 250;

  if (suppressSelector) {
    var st = document.createElement('style');
    st.textContent = suppressSelector + '{ display: none !important; }';
    document.head.appendChild(st);
  }

  function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]); }); }
  function money(v, c) { if (v == null) return ''; try { return new Intl.NumberFormat(undefined, { style: 'currency', currency: c || 'USD' }).format(v); } catch (e) { return '$' + Number(v).toFixed(2); } }

  var panel = document.createElement('div');
  panel.className = 'dynasort-predict-panel';
  panel.setAttribute('hidden', '');
  document.body.appendChild(panel);

  var activeInput = null, items = [], cursor = -1, lastReq = 0, debounceTimer = null;

  // Native <dialog> search modals (Horizon family) render in the browser top
  // layer, which sits above any z-index on body, and their open animations
  // add transforms that hijack position:fixed. Inside an open dialog the
  // panel therefore joins the modal's own layout (in-flow, static), anchored
  // where the suppressed native results sit. On plain pages (Dawn dropdowns)
  // it stays a floating fixed panel on body. Returns true when in-flow.
  function ensurePlacement() {
    var dlg = activeInput && activeInput.closest ? activeInput.closest('dialog') : null;
    if (dlg && dlg.open) {
      var anchor = suppressSelector ? dlg.querySelector(suppressSelector) : null;
      var target = anchor ? anchor.parentNode : (activeInput.form || dlg);
      if (panel.parentNode !== target || (anchor && panel.previousElementSibling !== anchor)) {
        if (anchor) { anchor.parentNode.insertBefore(panel, anchor.nextSibling); }
        else { target.appendChild(panel); }
      }
      panel.classList.add('dynasort-predict-panel--inflow');
      return true;
    }
    panel.classList.remove('dynasort-predict-panel--inflow');
    if (panel.parentNode !== document.body) document.body.appendChild(panel);
    return false;
  }
  function place() {
    if (!activeInput || panel.classList.contains('dynasort-predict-panel--inflow')) return;
    var r = activeInput.getBoundingClientRect();
    panel.style.left = r.left + 'px';
    panel.style.top = (r.bottom + 4) + 'px';
    panel.style.width = Math.max(260, r.width) + 'px';
    // Transformed/filtered ancestors (dialog open animations) turn position:fixed
    // into ancestor-relative. Measure the drift and subtract it.
    var pr = panel.getBoundingClientRect();
    var dx = pr.left - r.left, dy = pr.top - (r.bottom + 4);
    if (dx || dy) {
      panel.style.left = (r.left - dx) + 'px';
      panel.style.top = (r.bottom + 4 - dy) + 'px';
    }
  }
  function hide() { panel.setAttribute('hidden', ''); cursor = -1; }
  function show() {
    var inflow = ensurePlacement();
    panel.removeAttribute('hidden');
    if (inflow) { panel.style.left = panel.style.top = panel.style.width = ''; }
    else { place(); }
  }

  function render(data, q) {
    items = data.products || [];
    var cur = data.currency;
    if (!items.length) {
      panel.innerHTML = '<div class="dynasort-predict-empty">No suggestions for “' + esc(q) + '”.</div>' +
        '<a class="dynasort-predict-all" href="/search?q=' + encodeURIComponent(q) + '&type=product">See all results</a>';
      show(); return;
    }
    panel.innerHTML = items.map(function (c, i) {
      var img = c.featured_image ? '<img class="dynasort-predict-thumb" src="' + esc(c.featured_image) + '" alt="" loading="lazy">' : '<span class="dynasort-predict-thumb"></span>';
      var cmp = (c.compare_at_min != null) ? '<del>' + money(c.compare_at_min, cur) + '</del>' : '';
      return '<a class="dynasort-predict-item" href="' + esc(c.url) + '" data-idx="' + i + '">' + img +
        '<span class="dynasort-predict-meta"><span class="dynasort-predict-title">' + esc(c.title) + '</span>' +
        '<span class="dynasort-predict-price">' + cmp + money(c.price_min, cur) + '</span></span></a>';
    }).join('') +
      '<a class="dynasort-predict-all" href="/search?q=' + encodeURIComponent(q) + '&type=product">See all results for “' + esc(q) + '”</a>';
    cursor = -1;
    show();
  }

  function query(q) {
    var reqId = ++lastReq;
    fetch('/apps/dynasort/search?mode=predictive&limit=' + limit + '&q=' + encodeURIComponent(q) + '&_=' + Date.now(), { headers: { 'Accept': 'application/json' }, cache: 'no-store' })
      .then(function (r) { return r.json(); })
      .then(function (data) { if (reqId === lastReq && activeInput && activeInput.value.trim().length >= minChars) render(data, q); })
      .catch(function () {});
  }

  function onInput(e) {
    activeInput = e.target;
    var q = activeInput.value.trim();
    clearTimeout(debounceTimer);
    if (q.length < minChars) { hide(); return; }
    debounceTimer = setTimeout(function () { query(q); }, debounceMs);
  }

  function moveCursor(d) {
    var links = panel.querySelectorAll('.dynasort-predict-item');
    if (!links.length) return;
    if (cursor >= 0 && links[cursor]) links[cursor].classList.remove('is-active');
    cursor = (cursor + d + links.length) % links.length;
    links[cursor].classList.add('is-active');
    links[cursor].scrollIntoView({ block: 'nearest' });
  }

  function onKey(e) {
    if (panel.hasAttribute('hidden')) return;
    // stopPropagation keeps the theme's own (suppressed) predictive component
    // from double-handling the same keys against its hidden results.
    if (e.key === 'ArrowDown') { e.preventDefault(); e.stopPropagation(); moveCursor(1); }
    else if (e.key === 'ArrowUp') { e.preventDefault(); e.stopPropagation(); moveCursor(-1); }
    else if (e.key === 'Enter') {
      var links = panel.querySelectorAll('.dynasort-predict-item');
      if (cursor >= 0 && links[cursor]) { e.preventDefault(); e.stopPropagation(); window.location.href = links[cursor].getAttribute('href'); }
    } else if (e.key === 'Escape') { hide(); e.stopPropagation(); }
  }

  function attach(input) {
    if (input.__dsPredict) return;
    input.__dsPredict = true;
    input.setAttribute('autocomplete', 'off');
    input.addEventListener('input', onInput);
    input.addEventListener('focus', function (e) { activeInput = e.target; if (e.target.value.trim().length >= minChars) onInput(e); });
    input.addEventListener('keydown', onKey);
  }

  function scan() { Array.prototype.forEach.call(document.querySelectorAll(inputSelector), attach); }

  document.addEventListener('click', function (e) { if (!panel.contains(e.target) && e.target !== activeInput) hide(); });
  window.addEventListener('scroll', function () { if (!panel.hasAttribute('hidden')) place(); }, true);
  window.addEventListener('resize', function () { if (!panel.hasAttribute('hidden')) place(); });
  // dialog close doesn't bubble; capture-phase catches any search modal closing.
  document.addEventListener('close', function () { hide(); }, true);

  scan();
  if (window.MutationObserver) {
    var mo = new MutationObserver(function () { scan(); });
    mo.observe(document.body, { childList: true, subtree: true });
  }
})();
</script>

{% schema %}
{
  "name": "Dynasort Live Search",
  "target": "body",
  "settings": [
    { "type": "paragraph", "content": "Re-ranks the search suggestions dropdown using your active Dynasort search recipe. Selector values for every free Shopify theme: [Live Search theme selectors](https://docs.dynasort.io/search-re-ranking/live-search-selectors/)" },
    { "type": "text", "id": "input_selector", "label": "Search input selector", "default": "input[type=\"search\"], input[name=\"q\"]", "info": "The default matches nearly every theme. [Learn more](https://docs.dynasort.io/search-re-ranking/live-search-selectors/)" },
    { "type": "text", "id": "suppress_selector", "label": "Native dropdown selector to hide", "info": "Dawn family: #predictive-search-results. Horizon family: #search-modal .predictive-search-form__content. [All themes](https://docs.dynasort.io/search-re-ranking/live-search-selectors/)" },
    { "type": "range", "id": "min_chars", "label": "Minimum characters", "min": 1, "max": 5, "default": 2 },
    { "type": "range", "id": "max_results", "label": "Suggestions shown", "min": 3, "max": 10, "default": 6 },
    { "type": "range", "id": "debounce_ms", "label": "Keystroke debounce (ms)", "min": 100, "max": 600, "step": 50, "default": 250 }
  ]
}
{% endschema %}
