const overlay = document.querySelector("[data-site-search-overlay]");
const input = document.querySelector("[data-site-search-input]");
const resultsList = document.querySelector("[data-site-search-results]");
const help = document.querySelector("[data-site-search-help]");
const triggers = [...document.querySelectorAll("[data-site-search-trigger]")];
const closers = [...document.querySelectorAll("[data-site-search-close]")];

if (overlay && input && resultsList && help && triggers.length) {
  let entries = [];
  let indexLoaded = false;
  let indexLoadPromise = null;
  const visibleLimit = 10;
  let activeIndex = 0;
  let visibleResults = [];
  let lastOpener = null;

  const loadIndex = () => {
    if (indexLoaded) {
      return Promise.resolve(entries);
    }
    if (!indexLoadPromise) {
      indexLoadPromise = fetch("/search-index.json", { cache: "no-store" })
        .then((response) => {
          if (!response.ok) {
            throw new Error(`Search index request failed: ${response.status}`);
          }
          return response.json();
        })
        .then((data) => {
          entries = Array.isArray(data) ? data : [];
          indexLoaded = true;
          return entries;
        })
        .catch(() => {
          entries = [];
          indexLoaded = true;
          return entries;
        });
    }
    return indexLoadPromise;
  };

  const normalize = (value) => String(value || "")
    .toLowerCase()
    .replace(/&/g, " and ")
    .replace(/[^a-z0-9]+/g, " ")
    .trim()
    .replace(/\s+/g, " ");

  const tokensFor = (value) => normalize(value).split(" ").filter(Boolean);
  const initialsFor = (value) => tokensFor(value).map((token) => token[0]).join("");

  const boundedDistance = (left, right, limit) => {
    if (Math.abs(left.length - right.length) > limit) {
      return limit + 1;
    }
    const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
    for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
      const current = [leftIndex];
      let rowMinimum = current[0];
      for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
        const cost = left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1;
        const value = Math.min(
          previous[rightIndex] + 1,
          current[rightIndex - 1] + 1,
          previous[rightIndex - 1] + cost
        );
        current[rightIndex] = value;
        rowMinimum = Math.min(rowMinimum, value);
      }
      if (rowMinimum > limit) {
        return limit + 1;
      }
      previous.splice(0, previous.length, ...current);
    }
    return previous[right.length];
  };

  const scoreText = (text, rawQuery, exactScore, prefixScore, tokenScore, substringScore, initialScore, fuzzyBaseScore) => {
    const query = normalize(rawQuery);
    const title = normalize(text);
    if (!query || !title) {
      return 0;
    }

    const queryTokens = tokensFor(query);
    const titleTokens = tokensFor(title);

    if (title === query) {
      return exactScore;
    }
    if (title.startsWith(query)) {
      return prefixScore - title.length;
    }
    if (titleTokens.some((token) => token.startsWith(query))) {
      return tokenScore - title.length;
    }
    if (title.includes(query)) {
      return substringScore - title.indexOf(query);
    }

    const initials = initialsFor(title);
    if (query.length >= 2 && (initials.startsWith(query) || initials.includes(query))) {
      return initialScore - title.length;
    }

    let fuzzyScore = 0;
    for (const queryToken of queryTokens) {
      if (queryToken.length < 5) {
        continue;
      }
      const bestDistance = Math.min(...titleTokens.map((titleToken) => boundedDistance(queryToken, titleToken, 2)));
      if (bestDistance <= 2) {
        fuzzyScore += fuzzyBaseScore - bestDistance * 24;
      }
    }

    return fuzzyScore;
  };

  const scoreEntry = (entry, rawQuery) => {
    const titleScore = scoreText(entry.searchText || entry.title, rawQuery, 1000, 900, 780, 680, 560, 120);
    const locationScore = scoreText(entry.locationSearchText || "", rawQuery, 520, 500, 470, 430, 360, 90);
    return Math.max(titleScore, locationScore);
  };

  const suggestedEntries = () => entries.slice(0, visibleLimit).map((entry, index) => ({
    ...entry,
    score: visibleLimit - index
  }));

  const insertByScore = (collection, scoredEntry) => {
    let inserted = false;
    for (let index = 0; index < collection.length; index += 1) {
      const current = collection[index];
      if (scoredEntry.score > current.score) {
        collection.splice(index, 0, scoredEntry);
        inserted = true;
        break;
      }
      if (scoredEntry.score === current.score && scoredEntry.title.localeCompare(current.title) < 0) {
        collection.splice(index, 0, scoredEntry);
        inserted = true;
        break;
      }
    }
    if (!inserted) {
      collection.push(scoredEntry);
    }
  };

  const matchedEntries = (query) => {
    const matches = [];
    for (const entry of entries) {
      const score = scoreEntry(entry, query);
      if (score > 0) {
        insertByScore(matches, { ...entry, score });
      }
    }
    return matches;
  };

  const resultSubtitle = (entry) => entry.subtitle || (
    entry.type === "room" ? "Room page" : entry.type === "city" ? "City guide" : "Company page"
  );
  const resultType = (entry) => entry.type === "room" ? "Room" : entry.type === "city" ? "City" : "Company";

  const syncActiveResult = () => {
    for (const result of resultsList.querySelectorAll("[data-site-search-result]")) {
      result.setAttribute("aria-selected", Number(result.dataset.siteSearchResult) === activeIndex ? "true" : "false");
    }
  };

  const render = () => {
    if (!indexLoaded) {
      visibleResults = [];
      resultsList.innerHTML = '<li class="site-search-empty">Loading search...</li>';
      help.textContent = "Loading rooms, cities, and companies.";
      return;
    }

    const query = input.value;
    const hasQuery = normalize(query).length > 0;
    const matches = hasQuery ? matchedEntries(query) : suggestedEntries();
    visibleResults = matches.slice(0, visibleLimit);
    activeIndex = Math.min(activeIndex, Math.max(visibleResults.length - 1, 0));

    resultsList.innerHTML = "";

    if (!visibleResults.length) {
      help.textContent = "No matching rooms, cities, or companies. Try fewer words.";
      resultsList.innerHTML = '<li class="site-search-empty">No matching rooms, cities, or companies</li>';
      return;
    }

    help.textContent = hasQuery
      ? `${matches.length} ${matches.length === 1 ? "match" : "matches"} found.`
      : "Suggested rooms, cities, and companies.";

    for (const [index, entry] of visibleResults.entries()) {
      const item = document.createElement("li");
      const link = document.createElement("a");
      const marker = document.createElement("span");
      const copy = document.createElement("span");
      const title = document.createElement("strong");
      const subtitle = document.createElement("span");

      item.className = "site-search-result";
      link.href = entry.href;
      link.dataset.siteSearchResult = String(index);
      link.setAttribute("aria-selected", index === activeIndex ? "true" : "false");
      marker.className = "site-search-result-type";
      marker.textContent = resultType(entry);
      copy.className = "site-search-result-copy";
      title.textContent = entry.title;
      subtitle.textContent = resultSubtitle(entry);
      copy.append(title, subtitle);
      link.append(marker, copy);
      item.append(link);
      resultsList.append(item);
    }

    if (matches.length > visibleLimit) {
      const overflow = document.createElement("li");
      overflow.className = "site-search-overflow";
      overflow.textContent = "Keep typing to narrow results.";
      resultsList.append(overflow);
    }

    syncActiveResult();
  };

  const closeSearch = () => {
    overlay.hidden = true;
    document.documentElement.classList.remove("site-search-open");
    input.value = "";
    activeIndex = 0;
    render();
    if (lastOpener) {
      lastOpener.focus();
    }
  };

  const openSearch = (opener) => {
    lastOpener = opener;
    overlay.hidden = false;
    document.documentElement.classList.add("site-search-open");
    activeIndex = 0;
    render();
    loadIndex().then(render);
    window.requestAnimationFrame(() => input.focus());
  };

  triggers.forEach((trigger) => {
    trigger.addEventListener("click", (event) => {
      event.preventDefault();
      openSearch(trigger);
    });
  });

  closers.forEach((closer) => {
    closer.addEventListener("click", closeSearch);
  });

  input.addEventListener("input", () => {
    activeIndex = 0;
    render();
  });

  overlay.addEventListener("keydown", (event) => {
    if (event.key === "Escape") {
      event.preventDefault();
      closeSearch();
      return;
    }

    if (!visibleResults.length) {
      return;
    }

    if (event.key === "ArrowDown") {
      event.preventDefault();
      activeIndex = (activeIndex + 1) % visibleResults.length;
      syncActiveResult();
    }

    if (event.key === "ArrowUp") {
      event.preventDefault();
      activeIndex = (activeIndex - 1 + visibleResults.length) % visibleResults.length;
      syncActiveResult();
    }

    if (event.key === "Enter") {
      event.preventDefault();
      const activeResult = visibleResults[activeIndex];
      if (activeResult) {
        window.location.assign(activeResult.href);
      }
    }
  });

  resultsList.addEventListener("click", (event) => {
    const result = event.target.closest("[data-site-search-result]");
    if (!result) {
      return;
    }
    event.preventDefault();
    const selectedResult = visibleResults[Number(result.dataset.siteSearchResult)];
    if (selectedResult) {
      window.location.assign(selectedResult.href);
    }
  });

  resultsList.addEventListener("mouseover", (event) => {
    const result = event.target.closest("[data-site-search-result]");
    if (!result) {
      return;
    }
    activeIndex = Number(result.dataset.siteSearchResult);
    syncActiveResult();
  });

  render();
}
