import { useCallback, useEffect, useMemo, useRef, useState } from "react";

import { type NodeRole,roleTitles } from "../../../types";
import type { IMapNode } from "../lib/types";
import { nodeColor } from "../lib/utils";

export function MapSearchBar({
  nodes,
  onSelect,
}: {
  nodes: Record<string, IMapNode>;
  onSelect: (nodeId: string) => void;
}) {
  const [query, setQuery] = useState("");
  const [debouncedQuery, setDebouncedQuery] = useState("");
  const [open, setOpen] = useState(false);
  const [highlightIdx, setHighlightIdx] = useState(0);
  const inputRef = useRef<HTMLInputElement>(null);
  const listRef = useRef<HTMLDivElement>(null);

  // Debounce the (up to ~3k-node) scan so typing stays smooth.
  useEffect(() => {
    const t = setTimeout(() => setDebouncedQuery(query), 150);
    return () => clearTimeout(t);
  }, [query]);

  // Read the node list through a ref and key the scan on (query, node count,
  // coarse tick) instead of `nodes` identity: every ~400ms live flush swaps
  // the object, and re-running the full 3k-entry scan (plus dropping the
  // results identity) on each one is wasted work — results a few seconds
  // stale are fine here.
  const nodesRef = useRef(nodes);
  nodesRef.current = nodes;
  const nodeCount = Object.keys(nodes).length;

  // Bound the staleness: renames / new GPS fixes / online flips don't change
  // the node count, so refresh an open result list every few seconds too.
  const [refreshTick, setRefreshTick] = useState(0);
  const listActive = open && Boolean(debouncedQuery.trim());
  useEffect(() => {
    if (!listActive) return;
    const t = setInterval(() => setRefreshTick((v) => v + 1), 4_000);
    return () => clearInterval(t);
  }, [listActive]);

  const results = useMemo(() => {
    if (!debouncedQuery.trim() || nodeCount === 0) return [];
    const q = debouncedQuery.toLowerCase();
    return Object.entries(nodesRef.current)
      .filter(([id, n]) => {
        if (!n.map_position) return false;
        return (
          id.toLowerCase().includes(q) ||
          n.shortname?.toLowerCase().includes(q) ||
          n.longname?.toLowerCase().includes(q)
        );
      })
      .slice(0, 20)
      .map(([id, n]) => ({ id, node: n }));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [debouncedQuery, nodeCount, refreshTick]);

  // Reset keyboard highlight only when the query itself changes — resetting on
  // results identity snapped the highlight back to row 0 mid-navigation.
  useEffect(() => setHighlightIdx(0), [debouncedQuery]);
  // …but clamp it when the list shrinks under the cursor (node joined/left),
  // or Enter would silently do nothing on an out-of-range index.
  useEffect(() => {
    setHighlightIdx((i) => Math.min(i, Math.max(results.length - 1, 0)));
  }, [results]);

  useEffect(() => {
    const el = listRef.current?.children[highlightIdx] as HTMLElement | undefined;
    el?.scrollIntoView({ block: "nearest" });
  }, [highlightIdx]);

  const selectNode = useCallback(
    (id: string) => {
      onSelect(id);
      setQuery("");
      setOpen(false);
      inputRef.current?.blur();
    },
    [onSelect],
  );

  const onKeyDown = useCallback(
    (e: React.KeyboardEvent) => {
      if (e.key === "ArrowDown") {
        e.preventDefault();
        setHighlightIdx((i) => Math.min(i + 1, results.length - 1));
      } else if (e.key === "ArrowUp") {
        e.preventDefault();
        setHighlightIdx((i) => Math.max(i - 1, 0));
      } else if (e.key === "Enter" && results[highlightIdx]) {
        e.preventDefault();
        selectNode(results[highlightIdx].id);
      } else if (e.key === "Escape") {
        setOpen(false);
        inputRef.current?.blur();
      }
    },
    [results, highlightIdx, selectNode],
  );

  // Global "/" focuses search
  useEffect(() => {
    const handler = (e: KeyboardEvent) => {
      if (e.key === "/" && !e.ctrlKey && !e.metaKey) {
        const tag = (e.target as HTMLElement)?.tagName;
        if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
        e.preventDefault();
        inputRef.current?.focus();
      }
    };
    document.addEventListener("keydown", handler);
    return () => document.removeEventListener("keydown", handler);
  }, []);

  return (
    <div
      className="fixed top-3 left-[calc(var(--map-pad)+1rem)] z-30 transition-[left,width] duration-200
        w-36 sm:w-64
        focus-within:w-[calc(100vw-2rem)] focus-within:z-50
        sm:focus-within:w-64 sm:focus-within:z-30"
    >
      <div className="relative">
        <svg
          className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-gray-500 pointer-events-none"
          fill="none"
          stroke="currentColor"
          viewBox="0 0 24 24"
          aria-hidden="true"
        >
          <path
            strokeLinecap="round"
            strokeLinejoin="round"
            strokeWidth={2}
            d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
          />
        </svg>
        <input
          ref={inputRef}
          type="text"
          value={query}
          onChange={(e) => {
            setQuery(e.target.value);
            setOpen(true);
          }}
          onFocus={() => setOpen(true)}
          onBlur={() => setTimeout(() => setOpen(false), 150)}
          onKeyDown={onKeyDown}
          placeholder="Search nodes… (press /)"
          aria-label="Search nodes"
          role="combobox"
          aria-expanded={open && query.trim() !== "" && results.length > 0}
          aria-controls="node-search-listbox"
          aria-autocomplete="list"
          aria-activedescendant={
            open && results[highlightIdx] ? `node-search-opt-${results[highlightIdx].id}` : undefined
          }
          className="w-full pl-8 pr-3 py-1.5 rounded-xl text-xs
            bg-gray-900/95 border border-white/10 shadow-2xl
            text-gray-200 placeholder-gray-500
            focus:outline-none focus:border-cyan-500/50 focus:ring-1 focus:ring-cyan-500/50"
        />
      </div>

      {open && query.trim() !== "" && (
        <div
          ref={listRef}
          id="node-search-listbox"
          role="listbox"
          aria-label="Node search results"
          className="mt-1 max-h-64 overflow-y-auto rounded-xl
            bg-gray-900/90 backdrop-blur-xl border border-white/10 shadow-2xl"
        >
          {results.length === 0 && (
            <div className="px-3 py-2 text-xs text-gray-500">No matching nodes</div>
          )}
          {results.map(({ id, node }, i) => {
            const roleVal = (node as any).role as number | undefined;
            const roleName = roleVal != null ? roleTitles[roleVal as NodeRole]?.title : null;
            const dotColor = nodeColor(roleVal, Boolean(node.online));
            return (
              <button
                key={id}
                id={`node-search-opt-${id}`}
                type="button"
                role="option"
                aria-selected={i === highlightIdx}
                onMouseDown={(e) => e.preventDefault()}
                onClick={() => selectNode(id)}
                className={`w-full text-left px-3 py-1.5 text-xs flex items-center gap-2 transition-colors ${
                  i === highlightIdx
                    ? "bg-white/10 text-gray-100"
                    : "text-gray-400 hover:bg-white/5"
                }`}
              >
                <span
                  className="w-2 h-2 rounded-full shrink-0"
                  style={{ backgroundColor: dotColor }}
                  aria-hidden="true"
                />
                <span className="truncate font-medium text-gray-200">
                  {node.shortname || id}
                </span>
                {node.longname && node.longname !== node.shortname && (
                  <span className="truncate text-gray-500">{node.longname}</span>
                )}
                {roleName && (
                  <span className="ml-auto shrink-0 text-[10px] text-gray-500">{roleName}</span>
                )}
              </button>
            );
          })}
        </div>
      )}
    </div>
  );
}
