import clsx from "clsx";
import type { TFunction } from "i18next";
import { Search } from "lucide-react";
import * as React from "react";
import {
	Button,
	Dialog,
	DialogTrigger,
	ListBox,
	ListBoxItem,
	Modal,
	ModalOverlay,
	Radio,
	RadioGroup,
} from "react-aria-components";
import { useTranslation } from "react-i18next";
import { useFetcher, useSearchParams } from "react-router";
import { Avatar } from "~/components/Avatar";
import { Image } from "~/components/Image";
import { Input } from "~/components/Input";
import { LocaleTime } from "~/components/LocaleTime";
import type { SearchLoaderData } from "~/features/search/routes/search";
import { useDebounce } from "~/hooks/useDebounce";
import type { MainWeaponId } from "~/modules/in-game-lists/types";
import {
	navIconUrl,
	teamPage,
	tournamentOrganizationPage,
	userPage,
	weaponCategoryUrl,
} from "~/utils/urls";
import styles from "./GlobalSearch.module.css";
import {
	filterWeaponResults,
	getRecentWeapons,
	type SelectedWeapon,
	saveRecentWeapon,
	WeaponDestinationMenu,
	WeaponResultsList,
	weaponToSelectedWeapon,
} from "./WeaponSearch";

const SEARCH_TYPES = [
	"weapons",
	"users",
	"teams",
	"organizations",
	"tournaments",
] as const;
type SearchType = (typeof SEARCH_TYPES)[number];

const SEARCH_TYPE_TO_PREFIX: Record<SearchType, string> = {
	weapons: "w",
	users: "u",
	teams: "t",
	organizations: "o",
	tournaments: "to",
};

const STORAGE_KEY = "global-search-search-type";

function searchTypeIconPath(type: SearchType): string {
	if (type === "weapons") {
		return weaponCategoryUrl("SHOOTERS");
	}
	const navIcons: Record<Exclude<SearchType, "weapons">, string> = {
		users: "u",
		teams: "t",
		organizations: "medal",
		tournaments: "calendar",
	};
	return navIconUrl(navIcons[type]);
}

function getInitialSearchType(): SearchType {
	if (typeof window === "undefined") return "weapons";
	try {
		const stored = localStorage.getItem(STORAGE_KEY);
		if (stored && SEARCH_TYPES.includes(stored as SearchType)) {
			return stored as SearchType;
		}
	} catch {
		// localStorage may be unavailable
	}
	return "weapons";
}

export function GlobalSearch() {
	const { t } = useTranslation(["common"]);
	// TODO: use zod validated search params
	const [searchParams, setSearchParams] = useSearchParams();
	const [isMac, setIsMac] = React.useState(false);

	const searchParamOpen = searchParams.get("search") === "open";
	const searchParamType = searchParams.get("type");
	const searchParamWeapon = searchParams.get("weapon");
	const initialSearchType =
		searchParamType && SEARCH_TYPES.includes(searchParamType as SearchType)
			? (searchParamType as SearchType)
			: null;

	const [isOpen, setIsOpen] = React.useState(searchParamOpen);

	React.useEffect(() => {
		if (searchParamOpen) {
			setIsOpen(true);
		}
	}, [searchParamOpen]);

	React.useEffect(() => {
		setIsMac(/Mac|iPhone|iPad|iPod/.test(navigator.userAgent));
	}, []);

	React.useEffect(() => {
		const handleKeyDown = (e: KeyboardEvent) => {
			const modifierKey = isMac ? e.metaKey : e.ctrlKey;
			if (modifierKey && e.key === "k") {
				e.preventDefault();
				setIsOpen(true);
			}
		};

		document.addEventListener("keydown", handleKeyDown);
		return () => document.removeEventListener("keydown", handleKeyDown);
	}, [isMac]);

	const handleOpenChange = (open: boolean) => {
		setIsOpen(open);
		if (!open && (searchParamOpen || searchParamType || searchParamWeapon)) {
			const newParams = new URLSearchParams(searchParams);
			newParams.delete("search");
			newParams.delete("type");
			newParams.delete("weapon");
			setSearchParams(newParams, { replace: true });
		}
	};

	return (
		<DialogTrigger isOpen={isOpen} onOpenChange={handleOpenChange}>
			<Button className={styles.searchButton}>
				<Search className={styles.searchIcon} />
				<span className={styles.searchPlaceholder}>{t("common:search")}</span>
				<kbd className={styles.searchKbd}>{isMac ? "Cmd+K" : "Ctrl+K"}</kbd>
			</Button>
			<ModalOverlay className={styles.overlay} isDismissable>
				<Modal className={styles.modal}>
					<Dialog className={styles.dialog} aria-label={t("common:search")}>
						<GlobalSearchContent
							onClose={() => setIsOpen(false)}
							initialSearchType={initialSearchType}
							initialWeaponId={searchParamWeapon}
						/>
					</Dialog>
				</Modal>
			</ModalOverlay>
		</DialogTrigger>
	);
}

function resolveInitialWeapon(
	weaponIdStr: string | null,
	t: TFunction<["common", "weapons"]>,
): SelectedWeapon | null {
	if (!weaponIdStr) return null;
	const id = Number(weaponIdStr) as MainWeaponId;
	if (Number.isNaN(id)) return null;
	const name = t(`weapons:MAIN_${id}`);
	if (!name || name === `MAIN_${id}`) return null;
	return weaponToSelectedWeapon(id, t);
}

function GlobalSearchContent({
	onClose,
	initialSearchType,
	initialWeaponId,
}: {
	onClose: () => void;
	initialSearchType: SearchType | null;
	initialWeaponId: string | null;
}) {
	const { t } = useTranslation(["common", "weapons"]);
	const [query, setQuery] = React.useState("");
	const [searchType, setSearchType] = React.useState<SearchType>(
		initialSearchType ?? getInitialSearchType(),
	);
	const [selectedWeapon, setSelectedWeapon] =
		React.useState<SelectedWeapon | null>(
			resolveInitialWeapon(initialWeaponId, t),
		);

	const inputRef = React.useRef<HTMLInputElement>(null);
	const listBoxRef = React.useRef<HTMLDivElement>(null);
	const modifierKeyRef = React.useRef(false);

	const handleClickCapture = (e: React.MouseEvent) => {
		modifierKeyRef.current = e.metaKey || e.ctrlKey;
	};

	const fetcher = useFetcher<SearchLoaderData>();

	React.useEffect(() => {
		if (!selectedWeapon) {
			inputRef.current?.focus();
		}
	}, [selectedWeapon]);

	React.useEffect(() => {
		try {
			localStorage.setItem(STORAGE_KEY, searchType);
		} catch {
			// localStorage may be unavailable
		}
	}, [searchType]);

	useDebounce(
		() => {
			if (searchType === "weapons") return;
			if (query.length < 3) return;
			fetcher.load(
				`/search?q=${encodeURIComponent(query)}&type=${searchType}&limit=10`,
			);
		},
		300,
		[query, searchType],
	);

	const hasQuery = query.length >= 3;
	const fetchedQuery = fetcher.data?.query ?? null;
	const fetchedType = fetcher.data?.type ?? null;
	const isCurrentFetch =
		hasQuery && fetchedQuery === query && fetchedType === searchType;
	const results =
		hasQuery && fetchedType === searchType ? (fetcher.data?.results ?? []) : [];

	const weaponResults =
		searchType === "weapons" && hasQuery ? filterWeaponResults(query, t) : [];

	const recentWeapons: SelectedWeapon[] =
		searchType === "weapons"
			? getRecentWeapons().map((id) => weaponToSelectedWeapon(id, t))
			: [];

	const handleSelect = (key: React.Key) => {
		if (searchType === "weapons") {
			const weapon =
				weaponResults.find((w) => `weapon-${w.id}` === key) ??
				recentWeapons.find((w) => `weapon-${w.id}` === key);
			if (weapon) {
				setSelectedWeapon(weapon);
				setQuery("");
			}
			return;
		}

		if (!modifierKeyRef.current) {
			onClose();
		}
	};

	const handleSearchTypeChange = (value: string) => {
		setSearchType(value as SearchType);
		setSelectedWeapon(null);
	};

	const handleSearchTypeClick = () => {
		inputRef.current?.focus();
	};

	const handleQueryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
		const value = e.target.value;
		const separatorMatch = value.match(/^([a-zA-Z]+)\.$/);

		if (separatorMatch) {
			const typedPrefix = separatorMatch[1];
			const matchedType = SEARCH_TYPES.find(
				(type) => SEARCH_TYPE_TO_PREFIX[type] === typedPrefix,
			);
			if (matchedType) {
				setSearchType(matchedType);
				setSelectedWeapon(null);
				setQuery("");
				return;
			}
		}

		setQuery(value);
	};

	const handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
		const currentResults = searchType === "weapons" ? weaponResults : results;
		if (e.key === "ArrowDown" && currentResults.length > 0) {
			e.preventDefault();
			listBoxRef.current?.focus();
		}
	};

	const handleDestinationSelect = () => {
		if (!selectedWeapon) return;

		saveRecentWeapon(selectedWeapon.id);
		if (!modifierKeyRef.current) {
			onClose();
		}
	};

	const handleBackToWeaponSearch = () => {
		setSelectedWeapon(null);
	};

	if (searchType === "weapons" && selectedWeapon) {
		return (
			<div onClickCapture={handleClickCapture}>
				<WeaponDestinationMenu
					selectedWeapon={selectedWeapon}
					onBack={handleBackToWeaponSearch}
					onSelect={handleDestinationSelect}
					listBoxRef={listBoxRef}
				/>
			</div>
		);
	}

	return (
		<div onClickCapture={handleClickCapture}>
			<div className={styles.inputContainer}>
				<p className={styles.inputPrefix}>
					{`${SEARCH_TYPE_TO_PREFIX[searchType]}.`}
				</p>
				<Input
					ref={inputRef}
					className={styles.input}
					placeholder={t("common:search.placeholder")}
					value={query}
					onChange={handleQueryChange}
					onKeyDown={handleInputKeyDown}
					icon={<Search className={styles.inputIcon} />}
				/>
			</div>
			<div className={styles.searchTypeContainer}>
				<RadioGroup
					value={searchType}
					onChange={handleSearchTypeChange}
					orientation="horizontal"
					aria-label="Search type"
					className={styles.searchTypeRadioGroup}
				>
					{SEARCH_TYPES.map((type) => (
						<Radio
							key={type}
							value={type}
							className={styles.searchTypeRadioWrapper}
							onClick={handleSearchTypeClick}
						>
							{({ isSelected, isHovered, isFocusVisible }) => (
								<span
									className={clsx(styles.searchTypeRadio, {
										[styles.searchTypeRadioSelected]: isSelected,
										[styles.searchTypeRadioHovered]: isHovered && !isSelected,
										[styles.searchTypeRadioFocusVisible]: isFocusVisible,
									})}
								>
									<Image path={searchTypeIconPath(type)} size={18} alt="" />
									{t(`common:search.type.${type}`)}
								</span>
							)}
						</Radio>
					))}
				</RadioGroup>
			</div>
			{searchType === "weapons" ? (
				<WeaponResultsList
					weaponResults={weaponResults}
					recentWeapons={recentWeapons}
					onSelect={handleSelect}
					hasQuery={hasQuery}
					listBoxRef={listBoxRef}
				/>
			) : (
				<ListBox
					ref={listBoxRef}
					className={clsx(styles.listBox, "scrollbar")}
					aria-label={t("common:search")}
					onAction={handleSelect}
					renderEmptyState={() => {
						if (!hasQuery) {
							return (
								<div className={styles.emptyState}>
									{t("common:search.hint")}
								</div>
							);
						}
						if (!isCurrentFetch) {
							return (
								<div className={styles.emptyState}>
									{t("common:search.searching")}
								</div>
							);
						}
						return (
							<div className={styles.emptyState}>
								{t("common:search.noResults")}
							</div>
						);
					}}
				>
					{results.map((result) => (
						<ListBoxItem
							key={getResultKey(result)}
							id={getResultKey(result)}
							href={getResultHref(result)}
							className={styles.listBoxItem}
						>
							<ResultItem result={result} />
						</ListBoxItem>
					))}
				</ListBox>
			)}
		</div>
	);
}

type SearchResult = NonNullable<SearchLoaderData>["results"][number];

function getResultKey(result: SearchResult): string {
	switch (result.type) {
		case "user":
			return `user-${result.id}`;
		case "team":
			return `team-${result.customUrl}`;
		case "organization":
			return `org-${result.id}`;
		case "tournament":
			return `tournament-${result.id}`;
	}
}

function getResultHref(result: SearchResult): string {
	switch (result.type) {
		case "user":
			return userPage({
				discordId: result.discordId,
				customUrl: result.customUrl,
			});
		case "team":
			return teamPage(result.customUrl);
		case "organization":
			return tournamentOrganizationPage({ organizationSlug: result.slug });
		case "tournament":
			return `/to/${result.id}`;
	}
}

function ResultItem({ result }: { result: SearchResult }) {
	switch (result.type) {
		case "user":
			return (
				<div className={styles.resultItem}>
					<Avatar
						user={{
							discordId: result.discordId,
							discordAvatar: result.discordAvatar,
						}}
						size="xxs"
					/>
					<div className={styles.resultTexts}>
						<span className={styles.resultName}>{result.name}</span>
						{result.inGameName ? (
							<span className={styles.resultSecondary}>
								{result.inGameName}
							</span>
						) : null}
					</div>
				</div>
			);
		case "team":
			return (
				<div className={styles.resultItem}>
					<Avatar
						url={result.avatarUrl}
						size="xxs"
						identiconInput={result.name}
					/>
					<span className={styles.resultName}>{result.name}</span>
				</div>
			);
		case "organization":
			return (
				<div className={styles.resultItem}>
					<Avatar
						url={result.avatarUrl}
						size="xxs"
						identiconInput={result.name}
					/>
					<span className={styles.resultName}>{result.name}</span>
				</div>
			);
		case "tournament":
			return (
				<div className={styles.resultItem}>
					{result.logoUrl ? (
						<img
							src={result.logoUrl}
							alt=""
							width={24}
							height={24}
							className={styles.resultLogo}
						/>
					) : null}
					<div className={styles.resultTexts}>
						<span className={styles.resultName}>{result.name}</span>
						<LocaleTime
							date={result.startTime}
							options={{ day: "numeric", month: "long", year: "numeric" }}
							className={styles.resultSecondary}
						/>
					</div>
				</div>
			);
	}
}
