import { useCallback, useEffect, useRef, useState } from "react";
import { Action, ActionPanel, Icon, List } from "@vicinae/api";

// Where a search goes. A client-side bang redirector: it resolves
// DuckDuckGo-style bangs in the browser rather than round-tripping through a
// search engine to be bounced, and falls through to an ordinary search when
// there is no bang. Bang support is a property of this URL, not of this
// extension, which is why there is no bang parsing below.
// The engine is Panama's webSearchUrl preference (Applications page), the
// same key the fallback script command reads -- launcher-search-contract pins
// the two to one default. Read once per command launch; DuckDuckGo when the
// preference is unset or unreadable.
const DEFAULT_ENGINE = "https://duckduckgo.com/?q=";
const ENGINE = (() => {
  try {
    const configHome =
      process.env.XDG_CONFIG_HOME || `${process.env.HOME}/.config`;
    const stored = JSON.parse(
      require("fs").readFileSync(`${configHome}/panama/settings.json`, "utf8"),
    ).webSearchUrl;
    return typeof stored === "string" && stored.startsWith("https://")
      ? stored
      : DEFAULT_ENGINE;
  } catch {
    return DEFAULT_ENGINE;
  }
})();

// The suggestion endpoint Firefox's address bar uses. Answers with
// [query, [suggestion, ...], ...] and needs no key.
const SUGGEST = "https://suggestqueries.google.com/complete/search?client=firefox&q=";

// Long enough that typing a word is one request rather than one per keystroke,
// short enough that the list still feels attached to the keyboard.
const DEBOUNCE_MS = 150;

const searchUrl = (query: string) => ENGINE + encodeURIComponent(query);

export default function SearchCommand() {
  const [query, setQuery] = useState("");
  const [suggestions, setSuggestions] = useState<string[]>([]);
  const [loading, setLoading] = useState(false);

  // Both are refs rather than state: changing them must not re-render, and the
  // cleanup below needs whatever the latest one is, not the one captured when
  // an effect happened to run.
  const inFlight = useRef<AbortController | null>(null);
  const debounce = useRef<ReturnType<typeof setTimeout> | null>(null);

  // Typing is faster than the network, so responses can arrive out of order. An
  // older, slower answer landing after a newer one would leave the list showing
  // suggestions for a query that is no longer on screen -- so each new keystroke
  // aborts the request before it.
  useEffect(() => () => {
    inFlight.current?.abort();
    if (debounce.current) clearTimeout(debounce.current);
  }, []);

  const onSearchTextChange = useCallback((text: string) => {
    setQuery(text);
    if (debounce.current) clearTimeout(debounce.current);
    inFlight.current?.abort();

    const trimmed = text.trim();

    // A bang says *where* to search rather than what for, so Google's guesses
    // about it are noise: "!yt" suggests nothing anybody wants.
    if (trimmed === "" || trimmed.startsWith("!")) {
      setSuggestions([]);
      setLoading(false);
      return;
    }

    setLoading(true);
    debounce.current = setTimeout(async () => {
      const controller = new AbortController();
      inFlight.current = controller;
      try {
        const response = await fetch(SUGGEST + encodeURIComponent(trimmed), {
          signal: controller.signal,
        });
        const body = (await response.json()) as unknown;
        const returned = Array.isArray(body) ? body[1] : undefined;
        setSuggestions(
          Array.isArray(returned)
            ? returned.filter((entry): entry is string => typeof entry === "string")
            : [],
        );
      } catch {
        // Offline, rate-limited, or aborted by the next keystroke. The typed
        // query is still searchable, so this costs suggestions rather than the
        // command -- which is the right way round for something you reach for
        // when you already know what you want.
        setSuggestions([]);
      } finally {
        if (inFlight.current === controller) setLoading(false);
      }
    }, DEBOUNCE_MS);
  }, []);

  const trimmed = query.trim();

  return (
    <List
      isLoading={loading}
      onSearchTextChange={onSearchTextChange}
      searchBarPlaceholder="Search, or !bang to jump straight there"
    >
      {trimmed !== "" && (
        <List.Item
          title={trimmed}
          subtitle={trimmed.startsWith("!") ? "Bang" : "Search"}
          icon={Icon.MagnifyingGlass}
          actions={
            <ActionPanel>
              <Action.OpenInBrowser title="Search" url={searchUrl(trimmed)} />
            </ActionPanel>
          }
        />
      )}
      <List.Section title="Suggestions">
        {suggestions
          .filter((suggestion) => suggestion !== trimmed)
          .map((suggestion) => (
            <List.Item
              key={suggestion}
              title={suggestion}
              icon={Icon.MagnifyingGlass}
              actions={
                <ActionPanel>
                  <Action.OpenInBrowser title="Search" url={searchUrl(suggestion)} />
                </ActionPanel>
              }
            />
          ))}
      </List.Section>
    </List>
  );
}
