/* eslint-disable jsx-a11y/no-autofocus */
"use client";

import type {FC} from "react";
import type {ButtonProps} from "@heroui/react";

import {Command} from "cmdk";
import {useEffect, useState, useMemo, useCallback, useRef} from "react";
import {matchSorter} from "match-sorter";
import {Button, Kbd, Modal, ModalContent} from "@heroui/react";
import {CloseIcon} from "@heroui/shared-icons";
import {tv} from "tailwind-variants";
import {usePathname, useRouter} from "next/navigation";
import MultiRef from "react-multi-ref";
import {clsx} from "@heroui/shared-utils";
import scrollIntoView from "scroll-into-view-if-needed";
import {isAppleDevice, isWebKit} from "@react-aria/utils";
import {create} from "zustand";
import {isEmpty, intersectionBy} from "@heroui/shared-utils";
import {writeStorage, useLocalStorage} from "@rehooks/local-storage";
import {usePostHog} from "posthog-js/react";

import {
  DocumentCodeBoldIcon,
  HashBoldIcon,
  ChevronRightLinearIcon,
  SearchLinearIcon,
} from "./icons";

import searchData from "@/config/search-meta.json";
import {useUpdateEffect} from "@/hooks/use-update-effect";

const hideOnPaths = ["examples"];

export interface CmdkStore {
  isOpen: boolean;
  onClose: () => void;
  onOpen: () => void;
}

export const useCmdkStore = create<CmdkStore>((set) => ({
  isOpen: false,
  onClose: () => set({isOpen: false}),
  onOpen: () => set({isOpen: true}),
}));

const cmdk = tv({
  slots: {
    base: "max-h-full overflow-y-auto",
    header: [
      "flex",
      "items-center",
      "w-full",
      "px-4",
      "border-b",
      "border-default-400/50",
      "dark:border-default-100",
    ],
    searchIcon: "text-default-400 text-lg",
    input: [
      "w-full",
      "px-2",
      "h-14",
      "font-sans",
      "text-lg",
      "outline-solid outline-transparent",
      "rounded-none",
      "bg-transparent",
      "text-default-700",
      "placeholder-default-500",
      "dark:text-default-500",
      "dark:placeholder:text-default-300",
    ],
    list: ["px-4", "mt-2", "pb-4", "overflow-y-auto", "max-h-[50vh]"],
    itemWrapper: [
      "px-4",
      "mt-2",
      "group",
      "flex",
      "h-16",
      "justify-between",
      "items-center",
      "rounded-lg",
      "shadow-xs",
      "bg-content2/50",
      "active:opacity-70",
      "cursor-pointer",
      "transition-opacity",
      "data-[active=true]:bg-primary",
      "data-[active=true]:text-primary-foreground",
    ],
    leftWrapper: ["flex", "gap-3", "items-center", "w-full", "max-w-full"],
    leftIcon: [
      "text-default-500 dark:text-default-300",
      "group-data-[active=true]:text-primary-foreground",
    ],
    itemContent: ["flex", "flex-col", "gap-0", "justify-center", "max-w-[80%]"],
    itemParentTitle: [
      "text-default-400",
      "text-xs",
      "group-data-[active=true]:text-primary-foreground",
      "select-none",
    ],
    itemTitle: [
      "truncate",
      "text-default-500",
      "group-data-[active=true]:text-primary-foreground",
      "select-none",
    ],
    emptyWrapper: ["flex", "flex-col", "text-center", "items-center", "justify-center", "h-32"],
  },
});

interface SearchResultItem {
  content: string;
  objectID: string;
  url: string;
  type: "lvl1" | "lvl2" | "lvl3";
  hierarchy: {
    lvl1: string | null;
    lvl2?: string | null;
    lvl3?: string | null;
  };
}

const MATCH_KEYS = ["hierarchy.lvl1", "hierarchy.lvl2", "hierarchy.lvl3", "content"];
const RECENT_SEARCHES_KEY = "recent-searches";
const MAX_RECENT_SEARCHES = 10;
const MAX_RESULTS = 20;

export const Cmdk: FC<{}> = () => {
  const [query, setQuery] = useState("");
  const [activeItem, setActiveItem] = useState(0);
  const [menuNodes] = useState(() => new MultiRef<number, HTMLElement>());
  const slots = useMemo(() => cmdk(), []);

  const pathname = usePathname();

  const eventRef = useRef<"mouse" | "keyboard">();
  const listRef = useRef<HTMLDivElement>(null);
  const router = useRouter();

  const {isOpen, onClose, onOpen} = useCmdkStore();

  const posthog = usePostHog();

  const [recentSearches] = useLocalStorage<SearchResultItem[]>(RECENT_SEARCHES_KEY);

  const addToRecentSearches = (item: SearchResultItem) => {
    let searches = recentSearches ?? [];

    // Avoid adding the same search again
    if (!searches.find((i) => i.objectID === item.objectID)) {
      writeStorage(RECENT_SEARCHES_KEY, [item, ...searches].slice(0, MAX_RECENT_SEARCHES));
    } else {
      // Move the search to the top
      searches = searches.filter((i) => i.objectID !== item.objectID);
      writeStorage(RECENT_SEARCHES_KEY, [item, ...searches].slice(0, MAX_RECENT_SEARCHES));
    }
  };

  const prioritizeFirstLevelItems = (a: SearchResultItem, b: SearchResultItem) => {
    if (a.type === "lvl1") {
      return -1;
    } else if (b.type === "lvl1") {
      return 1;
    }

    return 0;
  };

  const results = useMemo<SearchResultItem[]>(
    function getResults() {
      if (query.length < 2) return [];

      const data = searchData as SearchResultItem[];

      const words = query.split(" ");

      if (words.length === 1) {
        return matchSorter(data, query, {
          keys: MATCH_KEYS,
          sorter: (matches) => {
            matches.sort((a, b) => prioritizeFirstLevelItems(a.item, b.item));

            return matches;
          },
        }).slice(0, MAX_RESULTS);
      }

      const matchesForEachWord = words.map((word) =>
        matchSorter(data, word, {
          keys: MATCH_KEYS,
          sorter: (matches) => {
            matches.sort((a, b) => prioritizeFirstLevelItems(a.item, b.item));

            return matches;
          },
        }),
      );

      const matches = intersectionBy(...matchesForEachWord, "objectID").slice(0, MAX_RESULTS);

      posthog.capture("Cmdk - Search", {
        name: "cmdk - search",
        action: "search",
        category: "cmdk",
        data: {query, words, matches: matches?.map((match) => match.url).join(", ")},
      });

      return matches;
    },
    [query],
  );

  const items = !isEmpty(results) ? results : (recentSearches ?? []);

  // Toggle the menu when ⌘K / CTRL K is pressed
  useEffect(() => {
    const onKeyDown = (e: KeyboardEvent) => {
      const hotkey = isAppleDevice() ? "metaKey" : "ctrlKey";

      if (e?.key?.toLowerCase() === "k" && e[hotkey]) {
        e.preventDefault();
        isOpen ? onClose() : onOpen();

        posthog.capture("Cmdk - Open/Close", {
          name: "cmdk - open/close",
          action: "keydown",
          category: "cmdk",
          data: isOpen ? "close" : "open",
        });
      }
    };

    document.addEventListener("keydown", onKeyDown);

    return () => {
      document.removeEventListener("keydown", onKeyDown);
    };
  }, [isOpen]);

  const onItemSelect = useCallback(
    (item: SearchResultItem) => {
      onClose();
      router.push(item.url);
      addToRecentSearches(item);

      posthog.capture("Cmdk - ItemSelect", {
        name: item.content,
        action: "click",
        category: "cmdk",
        data: item.url,
      });
    },
    [router, recentSearches],
  );

  const onInputKeyDown = useCallback(
    (e: React.KeyboardEvent) => {
      eventRef.current = "keyboard";
      switch (e.key) {
        case "ArrowDown": {
          e.preventDefault();
          if (activeItem + 1 < items.length) {
            setActiveItem(activeItem + 1);
          }
          break;
        }
        case "ArrowUp": {
          e.preventDefault();
          if (activeItem - 1 >= 0) {
            setActiveItem(activeItem - 1);
          }
          break;
        }
        case "Control":
        case "Alt":
        case "Shift": {
          e.preventDefault();
          break;
        }
        case "Enter": {
          if (items?.length <= 0) {
            break;
          }

          onItemSelect(items[activeItem]);

          break;
        }
      }
    },
    [activeItem, items, router],
  );

  useUpdateEffect(() => {
    setActiveItem(0);
  }, [query]);

  useUpdateEffect(() => {
    if (!listRef.current || eventRef.current === "mouse") return;
    const node = menuNodes.map.get(activeItem);

    if (!node) return;
    scrollIntoView(node, {
      scrollMode: "if-needed",
      behavior: "smooth",
      block: "end",
      inline: "end",
      boundary: listRef.current,
    });
  }, [activeItem]);

  const CloseButton = useCallback(
    ({
      onPress,
      className,
    }: {
      onPress?: ButtonProps["onPress"];
      className?: ButtonProps["className"];
    }) => {
      return (
        <Button
          isIconOnly
          className={clsx(
            "border data-[hover=true]:bg-content2 border-default-400 dark:border-default-100",
            className,
          )}
          radius="full"
          size="sm"
          variant="bordered"
          onPress={onPress}
        >
          <CloseIcon />
        </Button>
      );
    },
    [],
  );

  const renderItem = useCallback(
    (item: SearchResultItem, index: number, isRecent = false) => {
      const isLvl1 = item.type === "lvl1";

      const mainIcon = isRecent ? (
        <SearchLinearIcon className={slots.leftIcon()} size={20} strokeWidth={2} />
      ) : isLvl1 ? (
        <DocumentCodeBoldIcon className={slots.leftIcon()} />
      ) : (
        <HashBoldIcon className={slots.leftIcon()} />
      );

      return (
        <Command.Item
          key={item.objectID}
          ref={menuNodes.ref(index)}
          className={slots.itemWrapper()}
          data-active={index === activeItem}
          value={item.content}
          onMouseEnter={() => {
            eventRef.current = "mouse";

            setActiveItem(index);
          }}
          onSelect={() => {
            if (eventRef.current === "keyboard") {
              return;
            }

            onItemSelect(item);
          }}
        >
          <div className={slots.leftWrapper()}>
            {mainIcon}
            <div className={slots.itemContent()}>
              {!isLvl1 && <span className={slots.itemParentTitle()}>{item.hierarchy.lvl1}</span>}
              <p className={slots.itemTitle()}>{item.content}</p>
            </div>
          </div>

          <ChevronRightLinearIcon size={14} />
        </Command.Item>
      );
    },
    [activeItem, onItemSelect, CloseButton, slots],
  );

  const shouldOpen = !hideOnPaths.some((path) => pathname.includes(path));

  return (
    <Modal
      hideCloseButton
      backdrop="opaque"
      classNames={{
        base: [
          "mt-[20vh]",
          "border-small",
          "dark:border-default-100",
          "supports-backdrop-filter:bg-background/80",
          "dark:supports-backdrop-filter:bg-background/30",
          "supports-backdrop-filter:backdrop-blur-md",
          "supports-backdrop-filter:backdrop-saturate-150",
        ],
        backdrop: ["bg-black/80"],
      }}
      isOpen={isOpen && shouldOpen}
      motionProps={{
        onAnimationComplete: () => {
          if (!isOpen) {
            setQuery("");
          }
        },
      }}
      placement="top-center"
      scrollBehavior="inside"
      size="xl"
      onClose={() => onClose()}
    >
      <ModalContent>
        <Command className={slots.base()} label="Quick search command" shouldFilter={false}>
          <div className={slots.header()}>
            <SearchLinearIcon className={slots.searchIcon()} strokeWidth={2} />
            <Command.Input
              autoFocus={!isWebKit()}
              className={slots.input()}
              placeholder="Search documentation"
              value={query}
              onKeyDown={onInputKeyDown}
              onValueChange={setQuery}
            />
            {query.length > 0 && <CloseButton onPress={() => setQuery("")} />}
            <Kbd className="hidden md:block border-none px-2 py-1 ml-2 font-medium text-[0.6rem]">
              ESC
            </Kbd>
          </div>
          <Command.List ref={listRef} className={slots.list()} role="listbox">
            <Command.Empty>
              {query.length > 0 && (
                <div className={slots.emptyWrapper()}>
                  <div>
                    <p>No results for &quot;{query}&quot;</p>
                    {query.length === 1 ? (
                      <p className="text-default-400">
                        Try adding more characters to your search term.
                      </p>
                    ) : (
                      <p className="text-default-400">Try searching for something else.</p>
                    )}
                  </div>
                </div>
              )}
            </Command.Empty>

            {isEmpty(query) &&
              (isEmpty(recentSearches) ? (
                <div className={slots.emptyWrapper()}>
                  <p className="text-default-400">No recent searches</p>
                </div>
              ) : (
                recentSearches &&
                recentSearches.length > 0 && (
                  <Command.Group
                    heading={
                      <div className="flex items-center justify-between">
                        <p className="text-default-600">Recent</p>
                      </div>
                    }
                  >
                    {recentSearches.map((item, index) => renderItem(item, index, true))}
                  </Command.Group>
                )
              ))}

            {results.map((item, index) => renderItem(item, index))}
          </Command.List>
        </Command>
      </ModalContent>
    </Modal>
  );
};
