"use client";

import { XCircle } from "@phosphor-icons/react/dist/ssr/XCircle";
import { Badge } from "@pythnetwork/component-library/Badge";
import { Button } from "@pythnetwork/component-library/Button";
import { ModalDialog } from "@pythnetwork/component-library/ModalDialog";
import { SearchInput } from "@pythnetwork/component-library/SearchInput";
import { SingleToggleGroup } from "@pythnetwork/component-library/SingleToggleGroup";
import {
  Virtualizer,
  ListLayout,
} from "@pythnetwork/component-library/Virtualizer";
import {
  ListBox,
  ListBoxItem,
} from "@pythnetwork/component-library/unstyled/ListBox";
import {
  type ReactNode,
  useState,
  useCallback,
  useEffect,
  createContext,
  use,
  useMemo,
} from "react";
import { useCollator, useFilter } from "react-aria";

import styles from "./search-dialog.module.scss";
import { usePriceFeeds } from "../../hooks/use-price-feeds";
import { AssetClassTag } from "../AssetClassTag";
import { NoResults } from "../NoResults";
import { PriceFeedTag } from "../PriceFeedTag";
import { PublisherTag } from "../PublisherTag";
import { Score } from "../Score";

const CLOSE_DURATION_IN_SECONDS = 0.1;
const CLOSE_DURATION_IN_MS = CLOSE_DURATION_IN_SECONDS * 1000;

const INPUTS = new Set(["input", "select", "button", "textarea"]);

const SearchDialogOpenContext = createContext<
  ReturnType<typeof useSearchDialogStateContext> | undefined
>(undefined);

type Props = {
  children: ReactNode;
  publishers: ({
    id: string;
    averageScore: number;
  } & (
    | { name: string; icon: ReactNode }
    | { name?: undefined; icon?: undefined }
  ))[];
};

export const SearchDialogProvider = ({ children, publishers }: Props) => {
  const searchDialogState = useSearchDialogStateContext();
  const [search, setSearch] = useState("");
  const [type, setType] = useState<ResultType | "">("");
  const collator = useCollator();
  const filter = useFilter({ sensitivity: "base", usage: "search" });
  const feeds = usePriceFeeds();

  const close = useCallback(() => {
    searchDialogState.close();
    setTimeout(() => {
      setSearch("");
      setType("");
    }, CLOSE_DURATION_IN_MS);
  }, [searchDialogState, setSearch, setType]);

  const handleOpenChange = useCallback(
    (isOpen: boolean) => {
      if (!isOpen) {
        close();
      }
    },
    [close],
  );

  const results = useMemo(
    () =>
      [
        ...(type === ResultType.Publisher
          ? []
          : feeds
              .entries()
              .filter(([, { displaySymbol }]) =>
                filter.contains(displaySymbol, search),
              )
              .map(([symbol, feed]) => ({
                type: ResultType.PriceFeed as const,
                id: symbol,
                ...feed,
              }))),
        ...(type === ResultType.PriceFeed
          ? []
          : publishers
              .filter(
                (publisher) =>
                  filter.contains(publisher.id, search) ||
                  (publisher.name && filter.contains(publisher.name, search)),
              )
              .map((publisher) => ({
                type: ResultType.Publisher as const,
                ...publisher,
              }))),
      ].sort((a, b) =>
        collator.compare(
          a.type === ResultType.PriceFeed ? a.displaySymbol : (a.name ?? a.id),
          b.type === ResultType.PriceFeed ? b.displaySymbol : (b.name ?? b.id),
        ),
      ),
    [feeds, publishers, collator, filter, search, type],
  );

  return (
    <>
      <SearchDialogOpenContext value={searchDialogState}>
        {children}
      </SearchDialogOpenContext>
      <ModalDialog
        key="search-modal"
        isOpen={searchDialogState.isOpen}
        onOpenChange={handleOpenChange}
        overlayVariants={{
          unmounted: { backgroundColor: "#00000000" },
          hidden: { backgroundColor: "#00000000" },
          visible: { backgroundColor: "#00000080" },
        }}
        overlayClassName={styles.modalOverlay ?? ""}
        variants={{
          visible: {
            y: 0,
            transition: { type: "spring", duration: 0.8, bounce: 0.35 },
          },
          hidden: {
            y: "calc(-100% - 8rem)",
            transition: { ease: "linear", duration: CLOSE_DURATION_IN_SECONDS },
          },
          unmounted: {
            y: "calc(-100% - 8rem)",
          },
        }}
        className={styles.searchMenu ?? ""}
        aria-label="Search"
      >
        <div className={styles.searchBar}>
          <div className={styles.left}>
            <SearchInput
              size="md"
              width={90}
              placeholder="Asset symbol, publisher name or id"
              value={search}
              onChange={setSearch}
              // eslint-disable-next-line jsx-a11y/no-autofocus
              autoFocus
            />
            <SingleToggleGroup
              selectedKey={type}
              // @ts-expect-error react-aria coerces everything to Key for some reason...
              onSelectionChange={setType}
              items={[
                { id: "", children: "All" },
                { id: ResultType.PriceFeed, children: "Price Feeds" },
                { id: ResultType.Publisher, children: "Publishers" },
              ]}
            />
          </div>
          <Button
            className={styles.closeButton ?? ""}
            beforeIcon={(props) => <XCircle weight="fill" {...props} />}
            slot="close"
            hideText
            rounded
            variant="ghost"
            size="sm"
          >
            Close
          </Button>
        </div>
        <div className={styles.body}>
          <Virtualizer layout={new ListLayout()}>
            <ListBox
              aria-label="Search"
              items={results}
              className={styles.listbox ?? ""}
              // eslint-disable-next-line jsx-a11y/no-autofocus
              autoFocus={false}
              // @ts-expect-error looks like react-aria isn't exposing this
              // property in the typescript types correctly...
              shouldFocusOnHover
              onAction={close}
              emptyState={
                <NoResults
                  query={search}
                  onClearSearch={() => {
                    setSearch("");
                  }}
                />
              }
            >
              {(result) => (
                <ListBoxItem
                  textValue={
                    result.type === ResultType.PriceFeed
                      ? result.displaySymbol
                      : (result.name ?? result.id)
                  }
                  className={styles.item ?? ""}
                  href={`${result.type === ResultType.PriceFeed ? "/price-feeds" : "/publishers"}/${encodeURIComponent(result.id)}`}
                  data-is-first={result.id === results[0]?.id ? "" : undefined}
                >
                  <div className={styles.itemType}>
                    <Badge
                      variant={
                        result.type === ResultType.PriceFeed
                          ? "warning"
                          : "info"
                      }
                      style="filled"
                      size="xs"
                    >
                      {result.type === ResultType.PriceFeed
                        ? "PRICE FEED"
                        : "PUBLISHER"}
                    </Badge>
                  </div>
                  {result.type === ResultType.PriceFeed ? (
                    <>
                      <PriceFeedTag
                        compact
                        symbol={result.id}
                        className={styles.itemTag}
                      />
                      <AssetClassTag symbol={result.id} />
                    </>
                  ) : (
                    <>
                      <PublisherTag
                        className={styles.itemTag}
                        compact
                        publisherKey={result.id}
                        {...(result.name && {
                          name: result.name,
                          icon: result.icon,
                        })}
                      />
                      <Score score={result.averageScore} />
                    </>
                  )}
                </ListBoxItem>
              )}
            </ListBox>
          </Virtualizer>
        </div>
      </ModalDialog>
    </>
  );
};

enum ResultType {
  PriceFeed,
  Publisher,
}

const useSearchDialogStateContext = () => {
  const [isOpen, setIsOpen] = useState(false);
  const toggleIsOpen = useCallback(() => {
    setIsOpen((value) => !value);
  }, [setIsOpen]);
  const close = useCallback(() => {
    setIsOpen(false);
  }, [setIsOpen]);
  const open = useCallback(() => {
    setIsOpen(true);
  }, [setIsOpen]);

  const handleKeyDown = useCallback(
    (event: KeyboardEvent) => {
      const activeElement = document.activeElement;
      const tagName = activeElement?.tagName.toLowerCase();
      const isEditing =
        !tagName ||
        INPUTS.has(tagName) ||
        (activeElement !== null &&
          "isContentEditable" in activeElement &&
          activeElement.isContentEditable);
      const isSlash = event.key === "/";
      // Meta key for mac, ctrl key for non-mac
      const isCtrlK = event.key === "k" && (event.metaKey || event.ctrlKey);

      if (!isEditing && (isSlash || isCtrlK)) {
        event.preventDefault();
        toggleIsOpen();
      }
    },
    [toggleIsOpen],
  );

  useEffect(() => {
    window.addEventListener("keydown", handleKeyDown);
    return () => {
      window.removeEventListener("keydown", handleKeyDown);
    };
  }, [handleKeyDown]);

  return {
    isOpen,
    setIsOpen,
    toggleIsOpen,
    open,
    close,
  };
};

const useSearchDialogState = () => {
  const value = use(SearchDialogOpenContext);
  if (value) {
    return value;
  } else {
    throw new NotInitializedError();
  }
};

export const useToggleSearchDialog = () => useSearchDialogState().toggleIsOpen;

class NotInitializedError extends Error {
  constructor() {
    super("This component must be contained within a <SearchDialogProvider>");
    this.name = "NotInitializedError";
  }
}
