import React, {
  useCallback,
  useEffect,
  useRef,
  useState,
  type KeyboardEvent,
} from "react";
import { createPortal } from "react-dom";
import { useRouter } from "next/router";
import MiniSearch, { type SearchResult } from "minisearch";
import posthog from "posthog-js";
import {
  MINISEARCH_OPTIONS,
  SEARCH_OPTIONS,
  rerankResults,
  expandSynonyms,
} from "@/lib/search-config";

// ---------------------------------------------------------------------------
// Lazy singleton for the search index (keyed by basePath so basePath changes don't reuse wrong index)
// ---------------------------------------------------------------------------
let indexPromise: Promise<MiniSearch> | null = null;
let indexPromiseBasePath: string | undefined = undefined;

function loadIndex(basePath: string = ""): Promise<MiniSearch> {
  const prefix = basePath ? basePath.replace(/\/$/, "") : "";
  const url = `${prefix}/llms-search-index.json`;
  if (indexPromise === null || indexPromiseBasePath !== basePath) {
    indexPromiseBasePath = basePath;
    indexPromise = fetch(url)
      .then((res) => {
        if (!res.ok)
          throw new Error(`Failed to load search index: ${res.status}`);
        return res.text();
      })
      .then((json) => MiniSearch.loadJSON(json, MINISEARCH_OPTIONS));
  }
  return indexPromise;
}

/**
 * Extract a short snippet of content around the first query match.
 * Falls back to the first ~120 characters if no match is found.
 */
function getContentSnippet(
  content: string | undefined,
  query: string,
  maxLen = 120,
): string {
  if (!content) return "";
  // Strip markdown headings, code fences, tables, and extra whitespace
  const plain = content
    .replace(/^#{1,6}\s+.*$/gm, "")
    .replace(/```[\s\S]*?```/g, "")
    .replace(/`[^`]*`/g, "")
    .replace(/^\|[-|\s:]+\|$/gm, "") // table separator rows
    .replace(
      /^\|.*\|$/gm,
      (
        row, // table data rows → cell text
      ) =>
        row
          .replace(/^\||\|$/g, "")
          .split("|")
          .map((c) => c.trim())
          .filter(Boolean)
          .join(", "),
    )
    .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
    .replace(/[*_~]+/g, "")
    .replace(/\n+/g, " ")
    .replace(/\s+/g, " ")
    .trim();
  if (!plain) return "";

  // Try to find a window around the first query term match
  const words = query
    .trim()
    .split(/\s+/)
    .filter((w) => w.length > 1);
  if (words.length > 0) {
    const escaped = words
      .map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
      .join("|");
    const re = new RegExp(escaped, "i");
    const matchIdx = plain.search(re);
    if (matchIdx >= 0) {
      const start = Math.max(0, matchIdx - 40);
      const end = Math.min(plain.length, start + maxLen);
      let snippet = plain.slice(start, end).trim();
      if (start > 0) snippet = "…" + snippet;
      if (end < plain.length) snippet = snippet + "…";
      return snippet;
    }
  }

  // Fallback: beginning of content
  if (plain.length <= maxLen) return plain;
  return plain.slice(0, maxLen).trim() + "…";
}

/** Convert a MiniSearch doc id to a Next.js route. */
function idToRoute(id: string): string {
  return (
    "/" +
    id
      .replace("hatchet://docs/", "")
      .replace(/\/index$/, "")
      .replace(/\/index#/, "#")
  );
}

/** Extract the page route (without anchor) from a result. */
function getPageRoute(result: SearchResult): string {
  return (result.pageRoute as string) || result.id.replace(/#.*$/, "");
}

/** Get the page title from a result. */
function getPageTitle(result: SearchResult): string {
  return (result.pageTitle as string) || (result.title as string) || result.id;
}

/** Group results by page, maintaining overall order by first appearance. */
function groupByPage(
  results: SearchResult[],
): Array<{ pageRoute: string; pageTitle: string; items: SearchResult[] }> {
  const groups: Array<{
    pageRoute: string;
    pageTitle: string;
    items: SearchResult[];
  }> = [];
  const seen = new Map<string, number>();

  for (const r of results) {
    const route = getPageRoute(r);
    const idx = seen.get(route);
    if (idx !== undefined) {
      groups[idx].items.push(r);
    } else {
      seen.set(route, groups.length);
      groups.push({
        pageRoute: route,
        pageTitle: getPageTitle(r),
        items: [r],
      });
    }
  }

  return groups;
}

// ---------------------------------------------------------------------------
// Detect Mac for keyboard shortcut display
// ---------------------------------------------------------------------------
function useIsMac() {
  const [isMac, setIsMac] = useState(false);
  useEffect(() => {
    setIsMac(/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform));
  }, []);
  return isMac;
}

// ---------------------------------------------------------------------------
// Highlight matches in text
// ---------------------------------------------------------------------------
/** Max number of words used for highlight regex (ReDoS prevention). */
const HIGHLIGHT_MAX_WORDS = 16;
/** Max total character length for the combined regex pattern. */
const HIGHLIGHT_MAX_PATTERN_LEN = 256;

function HighlightMatches({ text, query }: { text: string; query: string }) {
  if (!query.trim()) return <>{text}</>;

  try {
    // Build regex from individual query words for better highlighting.
    // Limit the number of words and total pattern length to prevent ReDoS.
    let words = query
      .trim()
      .split(/\s+/)
      .filter((w) => w.length > 1)
      .map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
      .slice(0, HIGHLIGHT_MAX_WORDS);
    if (words.length === 0) return <>{text}</>;

    // Trim the word list further if the joined pattern exceeds the length cap.
    let pattern = words.join("|");
    while (pattern.length > HIGHLIGHT_MAX_PATTERN_LEN && words.length > 1) {
      words = words.slice(0, -1);
      pattern = words.join("|");
    }
    if (pattern.length > HIGHLIGHT_MAX_PATTERN_LEN) return <>{text}</>;

    const re = new RegExp(`(${pattern})`, "i");
    const parts = text.split(new RegExp(`(${pattern})`, "ig"));
    return (
      <>
        {parts.map((part, i) =>
          re.test(part) ? (
            <span key={i} className="_text-primary-600">
              {part}
            </span>
          ) : (
            <React.Fragment key={i}>{part}</React.Fragment>
          ),
        )}
      </>
    );
  } catch {
    return <>{text}</>;
  }
}

// ---------------------------------------------------------------------------
// Spinner icon (matches Nextra's loading spinner)
// ---------------------------------------------------------------------------
function SpinnerIcon() {
  return (
    <svg
      className="_size-5 _animate-spin _text-gray-400"
      xmlns="http://www.w3.org/2000/svg"
      fill="none"
      viewBox="0 0 24 24"
    >
      <circle
        className="_opacity-25"
        cx="12"
        cy="12"
        r="10"
        stroke="currentColor"
        strokeWidth="4"
      />
      <path
        className="_opacity-75"
        fill="currentColor"
        d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
      />
    </svg>
  );
}

// ---------------------------------------------------------------------------
// Search component
// ---------------------------------------------------------------------------
export default function Search({ className }: { className?: string }) {
  const router = useRouter();
  const isMac = useIsMac();
  const inputRef = useRef<HTMLInputElement>(null);
  const containerRef = useRef<HTMLDivElement>(null);
  const listRef = useRef<HTMLUListElement>(null);

  const [focused, setFocused] = useState(false);
  const [query, setQuery] = useState("");
  const [results, setResults] = useState<SearchResult[]>([]);
  const [isOpen, setIsOpen] = useState(false);
  const [activeIndex, setActiveIndex] = useState(-1);
  const [indexReady, setIndexReady] = useState(false);
  const [loading, setLoading] = useState(false);
  const [dropdownPos, setDropdownPos] = useState<{
    top: number;
    right: number;
    width: number;
  } | null>(null);

  // ---------------------------------------------------------------------------
  // PostHog search-miss tracking
  // ---------------------------------------------------------------------------
  // Mutable ref tracks the current search session without triggering re-renders.
  // We capture events when the dropdown closes (isOpen → false).
  const searchSessionRef = useRef({
    query: "",
    resultCount: 0,
    clicked: false,
  });
  const prevIsOpenRef = useRef(false);

  // Fire PostHog events when the search dropdown closes
  useEffect(() => {
    if (prevIsOpenRef.current && !isOpen) {
      const { query: q, resultCount, clicked } = searchSessionRef.current;
      const trimmed = q.trim();
      if (trimmed) {
        if (resultCount === 0) {
          posthog.capture("docs_search_no_results", { query: trimmed });
        } else if (!clicked) {
          posthog.capture("docs_search_abandoned", {
            query: trimmed,
            result_count: resultCount,
          });
        }
      }
      searchSessionRef.current = { query: "", resultCount: 0, clicked: false };
    }
    prevIsOpenRef.current = isOpen;
  }, [isOpen]);

  const basePath = router.basePath ?? "";

  // Lazy-load the search index on first interaction (focus / open) rather
  // than on every page load.  The search-query effect below already handles
  // the case where the index isn't ready yet, so this is purely a preload
  // optimisation that fires as soon as the user clicks into the search box.
  const preloadTriggered = useRef(false);
  const preloadIndex = useCallback(() => {
    if (!preloadTriggered.current) {
      preloadTriggered.current = true;
      loadIndex(basePath).then(() => setIndexReady(true));
    }
  }, [basePath]);

  // Run the search when the query changes
  useEffect(() => {
    if (!query.trim()) {
      setResults([]);
      return;
    }

    function runSearch(idx: MiniSearch) {
      try {
        const expanded = expandSynonyms(query);
        const raw = idx.search(expanded, SEARCH_OPTIONS);
        // Rerank against the original query so title matching is accurate
        const reranked = rerankResults(raw, query).slice(0, 20);
        setResults(reranked);
        searchSessionRef.current.resultCount = reranked.length;
      } catch {
        // Gracefully handle invalid queries (e.g. punctuation-only input)
        setResults([]);
        searchSessionRef.current.resultCount = 0;
      }
    }

    if (!indexReady) {
      setLoading(true);
      loadIndex(basePath)
        .then((idx) => {
          setIndexReady(true);
          setLoading(false);
          runSearch(idx);
        })
        .catch(() => setLoading(false));
      return;
    }

    loadIndex(basePath)
      .then(runSearch)
      .catch(() => {});
  }, [query, indexReady, basePath]);

  // Global keyboard shortcut: / or Cmd/Ctrl+K
  useEffect(() => {
    function onKeyDown(e: globalThis.KeyboardEvent) {
      if (
        e.key === "/" &&
        !e.metaKey &&
        !e.ctrlKey &&
        !["INPUT", "TEXTAREA"].includes(
          (e.target as HTMLElement)?.tagName || "",
        )
      ) {
        e.preventDefault();
        preloadIndex();
        inputRef.current?.focus();
      }
      if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
        e.preventDefault();
        preloadIndex();
        inputRef.current?.focus();
      }
    }
    window.addEventListener("keydown", onKeyDown);
    return () => window.removeEventListener("keydown", onKeyDown);
  }, [preloadIndex]);

  // Close on outside click
  useEffect(() => {
    function onClick(e: MouseEvent) {
      const target = e.target as Node;
      if (
        containerRef.current &&
        !containerRef.current.contains(target) &&
        listRef.current &&
        !listRef.current.contains(target)
      ) {
        setIsOpen(false);
      } else if (
        containerRef.current &&
        !containerRef.current.contains(target) &&
        !listRef.current
      ) {
        setIsOpen(false);
      }
    }
    document.addEventListener("mousedown", onClick);
    return () => document.removeEventListener("mousedown", onClick);
  }, []);

  // Close on route change
  useEffect(() => {
    const handleRouteChange = () => {
      setIsOpen(false);
      setQuery("");
      inputRef.current?.blur();
    };
    router.events.on("routeChangeComplete", handleRouteChange);
    return () => router.events.off("routeChangeComplete", handleRouteChange);
  }, [router]);

  // Scroll active item into view
  useEffect(() => {
    if (activeIndex >= 0 && listRef.current) {
      const item = listRef.current.querySelector(
        `[data-result-index="${activeIndex}"]`,
      ) as HTMLElement;
      item?.scrollIntoView({ block: "nearest" });
    }
  }, [activeIndex]);

  const showDropdown = isOpen && query.trim().length > 0;
  const hasResults = results.length > 0;
  const grouped = hasResults ? groupByPage(results) : [];

  // Build a flat list of items for keyboard navigation
  const flatItems: SearchResult[] = grouped.flatMap((g) => g.items);

  // Compute dropdown position based on input bounding rect
  useEffect(() => {
    if (!showDropdown || !containerRef.current) {
      setDropdownPos(null);
      return;
    }
    const updatePos = () => {
      const rect = containerRef.current?.getBoundingClientRect();
      if (rect) {
        setDropdownPos({
          top: rect.bottom + 8,
          right: window.innerWidth - rect.right,
          width: Math.max(rect.width, 576),
        });
      }
    };
    updatePos();
    window.addEventListener("scroll", updatePos);
    window.addEventListener("resize", updatePos);
    return () => {
      window.removeEventListener("scroll", updatePos);
      window.removeEventListener("resize", updatePos);
    };
  }, [showDropdown]);

  const navigate = useCallback(
    (id: string) => {
      const route = idToRoute(id);
      searchSessionRef.current.clicked = true;
      posthog.capture("docs_search_result_clicked", {
        query: searchSessionRef.current.query.trim(),
        result_id: id,
        result_route: route,
      });
      setIsOpen(false);
      setQuery("");
      router.push(route);
    },
    [router],
  );

  const onKeyDown = useCallback(
    (e: KeyboardEvent<HTMLInputElement>) => {
      if (e.key === "ArrowDown") {
        e.preventDefault();
        setActiveIndex((i) => Math.min(i + 1, flatItems.length - 1));
      } else if (e.key === "ArrowUp") {
        e.preventDefault();
        setActiveIndex((i) => Math.max(i - 1, 0));
      } else if (e.key === "Enter") {
        e.preventDefault();
        const idx = activeIndex >= 0 ? activeIndex : 0;
        if (flatItems[idx]) navigate(flatItems[idx].id);
      } else if (e.key === "Escape") {
        setIsOpen(false);
        inputRef.current?.blur();
      }
    },
    [flatItems, activeIndex, navigate],
  );

  return (
    <div
      ref={containerRef}
      className={`_not-prose _relative _flex _items-center _text-gray-900 dark:_text-gray-300 ${className || ""}`}
    >
      {/* Search input */}
      <input
        ref={inputRef}
        type="search"
        autoComplete="off"
        spellCheck={false}
        placeholder="Search documentation…"
        value={query}
        onChange={(e) => {
          const val = e.target.value;
          setQuery(val);
          searchSessionRef.current.query = val;
          setIsOpen(true);
          setActiveIndex(-1);
        }}
        onFocus={() => {
          preloadIndex();
          setFocused(true);
          if (query.trim()) setIsOpen(true);
        }}
        onBlur={() => setFocused(false)}
        onKeyDown={onKeyDown}
        className={[
          "_rounded-lg _px-3 _py-2 _transition-colors",
          "_w-full md:_w-64",
          "_text-base _leading-tight md:_text-sm",
          focused
            ? "_bg-transparent nextra-focusable"
            : "_bg-black/[.05] dark:_bg-gray-50/10",
          "placeholder:_text-gray-500 dark:placeholder:_text-gray-400",
          "contrast-more:_border contrast-more:_border-current",
          "[&::-webkit-search-cancel-button]:_appearance-none",
        ].join(" ")}
      />

      {/* Keyboard shortcut indicator */}
      {!focused && !query && (
        <kbd
          className={[
            "_absolute _my-1.5 _select-none ltr:_right-1.5 rtl:_left-1.5",
            "_h-5 _rounded _bg-white _px-1.5 _font-mono _text-[11px] _font-medium _text-gray-500",
            "_border dark:_border-gray-100/20 dark:_bg-black/50",
            "contrast-more:_border-current contrast-more:_text-current contrast-more:dark:_border-current",
            "_pointer-events-none _flex _items-center _gap-1",
            "max-sm:_hidden",
          ].join(" ")}
        >
          {isMac ? (
            <>
              <span className="_text-xs">⌘</span> K
            </>
          ) : (
            "CTRL K"
          )}
        </kbd>
      )}

      {/* Results dropdown (portaled to body to escape overflow:hidden ancestors) */}
      {showDropdown &&
        dropdownPos &&
        typeof document !== "undefined" &&
        createPortal(
          <ul
            ref={listRef}
            style={{
              position: "fixed",
              top: dropdownPos.top,
              right: dropdownPos.right,
              width: dropdownPos.width,
              zIndex: 50,
              overflowY: "auto",
              maxHeight: "min(calc(100vh - 5rem), 400px)",
            }}
            className={[
              "nextra-search-results nextra-scrollbar",
              "_rounded-xl _py-2.5 _shadow-xl",
              "_border _border-gray-200 dark:_border-neutral-800",
              "_backdrop-blur-lg _bg-[rgb(var(--nextra-bg),.8)]",
              "contrast-more:_border contrast-more:_border-gray-900 contrast-more:dark:_border-gray-50",
              "_transition-opacity _opacity-100",
            ].join(" ")}
          >
            {loading && (
              <li className="_flex _select-none _justify-center _gap-2 _p-8 _text-center _text-sm _text-gray-400">
                <SpinnerIcon />
                Loading…
              </li>
            )}

            {!loading && !hasResults && (
              <li className="_flex _select-none _justify-center _gap-2 _p-8 _text-center _text-sm _text-gray-400">
                No results for &ldquo;{query}&rdquo;
              </li>
            )}

            {(() => {
              let flatIdx = 0;
              return grouped.map((group) => (
                <li key={group.pageRoute} className="_mt-1 first:_mt-0">
                  {/* Page title header */}
                  <div className="_mx-2.5 _mb-1 _mt-2 _select-none _border-b _border-black/10 _px-2.5 _pb-1.5 _text-xs _font-semibold _uppercase _tracking-wider _text-gray-500 dark:_border-white/20 dark:_text-gray-400 first:_mt-0">
                    <HighlightMatches text={group.pageTitle} query={query} />
                  </div>
                  {/* Section items */}
                  <ul>
                    {group.items.map((result) => {
                      const idx = flatIdx++;
                      return (
                        <li key={result.id} data-result-index={idx}>
                          <a
                            href={idToRoute(result.id)}
                            onClick={(e) => {
                              e.preventDefault();
                              navigate(result.id);
                            }}
                            onMouseEnter={() => setActiveIndex(idx)}
                            className={[
                              "_mx-2.5 _break-words _rounded-md",
                              "_block _scroll-m-12 _px-2.5 _py-2",
                              "_cursor-pointer",
                              "contrast-more:_border",
                              idx === activeIndex
                                ? "_text-primary-600 contrast-more:_border-current _bg-primary-500/10"
                                : "_text-gray-800 dark:_text-gray-300 contrast-more:_border-transparent",
                            ].join(" ")}
                          >
                            <div className="_text-base _font-semibold _leading-5">
                              <HighlightMatches
                                text={(result.title as string) || result.id}
                                query={query}
                              />
                            </div>
                            {getContentSnippet(
                              result.content as string,
                              query,
                            ) && (
                              <div className="excerpt _mt-1 _text-sm _leading-[1.35rem] _text-gray-600 dark:_text-gray-400 contrast-more:dark:_text-gray-50">
                                <HighlightMatches
                                  text={getContentSnippet(
                                    result.content as string,
                                    query,
                                  )}
                                  query={query}
                                />
                              </div>
                            )}
                          </a>
                        </li>
                      );
                    })}
                  </ul>
                </li>
              ));
            })()}
          </ul>,
          document.body,
        )}
    </div>
  );
}
