/*
 * Copyright (C) 2026 Fluxer Contributors
 *
 * This file is part of Fluxer.
 *
 * Fluxer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Fluxer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
 */

import {autoUpdate, FloatingPortal, flip, offset, size, useFloating} from '@floating-ui/react';
import {msg} from '@lingui/core/macro';
import {useLingui} from '@lingui/react/macro';
import type {IconProps} from '@phosphor-icons/react';
import {
	ChatCenteredDotsIcon,
	EnvelopeSimpleIcon,
	GlobeIcon,
	HashIcon,
	MagnifyingGlassIcon,
	UsersIcon,
	XIcon,
} from '@phosphor-icons/react';
import {DateTime} from 'luxon';
import {matchSorter} from 'match-sorter';
import {observer} from 'mobx-react-lite';
import React from 'react';
import * as ContextMenuActionCreators from '~/actions/ContextMenuActionCreators';
import {ChannelTypes} from '~/Constants';
import {ContextMenuCloseProvider} from '~/components/uikit/ContextMenu/ContextMenu';
import {MenuGroup} from '~/components/uikit/ContextMenu/MenuGroup';
import {MenuItemRadio} from '~/components/uikit/ContextMenu/MenuItemRadio';
import {Tooltip} from '~/components/uikit/Tooltip/Tooltip';
import {useParams} from '~/lib/router';
import type {ChannelRecord} from '~/records/ChannelRecord';
import type {GuildMemberRecord} from '~/records/GuildMemberRecord';
import type {GuildRecord} from '~/records/GuildRecord';
import type {UserRecord} from '~/records/UserRecord';
import ChannelSearchStore, {getChannelSearchContextId} from '~/stores/ChannelSearchStore';
import ChannelStore from '~/stores/ChannelStore';
import GuildMemberStore from '~/stores/GuildMemberStore';
import GuildStore from '~/stores/GuildStore';
import KeyboardModeStore from '~/stores/KeyboardModeStore';
import MemberSearchStore, {type SearchContext} from '~/stores/MemberSearchStore';
import type {SearchHistoryEntry} from '~/stores/SearchHistoryStore';
import SearchHistoryStore from '~/stores/SearchHistoryStore';
import SelectedChannelStore from '~/stores/SelectedChannelStore';
import SelectedGuildStore from '~/stores/SelectedGuildStore';
import UserStore from '~/stores/UserStore';
import * as NicknameUtils from '~/utils/NicknameUtils';
import type {SearchSegment} from '~/utils/SearchSegmentManager';
import type {MessageSearchScope, SearchFilterOption} from '~/utils/SearchUtils';
import {getSearchFilterOptions} from '~/utils/SearchUtils';
import {DEFAULT_SCOPE_VALUE, getScopeOptionsForChannel} from '../searchScopeOptions';
import {ChannelsSection} from './ChannelsSection';
import {DateSection} from './DateSection';
import {FiltersSection} from './FilterOption';
import {HistorySection} from './HistorySection';
import styles from './MessageSearchBar.module.css';
import {UsersSection} from './UsersSection';
import {ValuesSection} from './ValuesSection';

const SCOPE_ICON_COMPONENTS: Record<MessageSearchScope, React.ComponentType<IconProps>> = {
	current: HashIcon,
	all_dms: EnvelopeSimpleIcon,
	open_dms: ChatCenteredDotsIcon,
	all_guilds: GlobeIcon,
	all: UsersIcon,
	open_dms_and_all_guilds: UsersIcon,
};

const USER_FILTER_KEYS = new Set(['from', 'mentions', '-from', '-mentions']);

export interface SearchBarProps {
	channel?: ChannelRecord;
	value: string;
	onChange: (value: string, segments: Array<SearchSegment>) => void;
	onSearch: () => void;
	onClear: () => void;
	isResultsOpen?: boolean;
	onCloseResults?: () => void;
	inputRefExternal?: React.Ref<HTMLInputElement>;
}

type AutocompleteType = 'filters' | 'users' | 'channels' | 'values' | 'date' | 'history' | null;

interface SearchHints {
	usersByTag: Record<string, string>;
	channelsByName: Record<string, string>;
}

type AutocompleteOption =
	| SearchFilterOption
	| UserRecord
	| ChannelRecord
	| {value: string; label: string}
	| string
	| SearchHistoryEntry;

const filterRequiresValue = (filter: SearchFilterOption): boolean => {
	return Boolean(filter.requiresValue) || (filter.values?.length ?? 0) > 0;
};

function _deduplicateMembers(members: Array<GuildMemberRecord>): Array<GuildMemberRecord> {
	const seen = new Set<string>();
	const result: Array<GuildMemberRecord> = [];
	for (const member of members) {
		if (!seen.has(member.user.id)) {
			seen.add(member.user.id);
			result.push(member);
		}
	}
	return result;
}

function assignRef<T>(ref: React.Ref<T> | undefined, value: T | null): void {
	if (!ref) {
		return;
	}

	if (typeof ref === 'function') {
		ref(value);
		return;
	}

	(ref as React.MutableRefObject<T | null>).current = value;
}

function normalizeFilterKey(filterKey: string): string {
	return filterKey.replace(/^-/, '');
}

function isDateFilterKey(filterKey: string): boolean {
	switch (normalizeFilterKey(filterKey)) {
		case 'before':
		case 'after':
		case 'during':
		case 'on':
			return true;
		default:
			return false;
	}
}

function _isUserFilterKey(filterKey: string): boolean {
	switch (normalizeFilterKey(filterKey)) {
		case 'from':
		case 'mentions':
			return true;
		default:
			return false;
	}
}

function _isChannelFilterKey(filterKey: string): boolean {
	return normalizeFilterKey(filterKey) === 'in';
}

type GuildSearchMode = 'none' | 'current_guild' | 'all_guilds';

interface UserGuildSearchPlan {
	mode: GuildSearchMode;
	guildsToSearch: Array<GuildRecord> | null;
	priorityGuildId?: string;
	workerFilters: {friends?: boolean; guild?: string};
}

function getUserGuildSearchPlan(scope: MessageSearchScope, currentGuildId: string | undefined): UserGuildSearchPlan {
	const SCOPES_WITH_GUILDS = new Set<MessageSearchScope>(['current', 'all_guilds', 'all', 'open_dms_and_all_guilds']);
	const ALL_GUILDS_SCOPES = new Set<MessageSearchScope>(['all_guilds', 'all', 'open_dms_and_all_guilds']);

	if (!SCOPES_WITH_GUILDS.has(scope)) {
		return {
			mode: 'none',
			guildsToSearch: null,
			priorityGuildId: undefined,
			workerFilters: {},
		};
	}

	if (scope === 'current') {
		if (!currentGuildId) {
			return {
				mode: 'none',
				guildsToSearch: null,
				priorityGuildId: undefined,
				workerFilters: {},
			};
		}

		const guild = GuildStore.getGuild(currentGuildId);
		return {
			mode: 'current_guild',
			guildsToSearch: guild ? [guild] : [],
			priorityGuildId: currentGuildId,
			workerFilters: {guild: currentGuildId},
		};
	}

	if (ALL_GUILDS_SCOPES.has(scope)) {
		return {
			mode: 'all_guilds',
			guildsToSearch: GuildStore.getGuilds(),
			priorityGuildId: currentGuildId,
			workerFilters: {},
		};
	}

	return {
		mode: 'none',
		guildsToSearch: null,
		priorityGuildId: undefined,
		workerFilters: {},
	};
}

type MemberSearchBoosters = Record<string, number>;

function buildUserSearchBoosters(
	channel: ChannelRecord | undefined,
	currentGuildId: string | undefined,
	mode: GuildSearchMode,
) {
	const boosters: MemberSearchBoosters = {};

	if (
		channel &&
		(channel.type === ChannelTypes.DM ||
			channel.type === ChannelTypes.GROUP_DM ||
			channel.type === ChannelTypes.DM_PERSONAL_NOTES)
	) {
		for (const id of channel.recipientIds) {
			boosters[id] = Math.max(boosters[id] ?? 1, 3);
		}
	}

	if (mode === 'all_guilds' && currentGuildId) {
		const members = GuildMemberStore.getMembers(currentGuildId);
		const MAX_BOOSTED_MEMBERS = 300;
		for (let i = 0; i < members.length && i < MAX_BOOSTED_MEMBERS; i += 1) {
			const id = members[i]!.user.id;
			boosters[id] = Math.max(boosters[id] ?? 1, 2);
		}
	}

	return boosters;
}

export const MessageSearchBar = observer(
	({
		channel,
		value,
		onChange,
		onSearch,
		onClear,
		isResultsOpen = false,
		onCloseResults,
		inputRefExternal,
	}: SearchBarProps) => {
		const {i18n} = useLingui();
		const {guildId: routeGuildId} = useParams() as {guildId?: string};

		const [isFocused, setIsFocused] = React.useState(false);
		const [autocompleteType, setAutocompleteType] = React.useState<AutocompleteType>(null);
		const [selectedIndex, setSelectedIndex] = React.useState(-1);
		const [hoverIndex, setHoverIndex] = React.useState(-1);
		const [hasNavigated, setHasNavigated] = React.useState(false);
		const [hasInteracted, setHasInteracted] = React.useState(false);
		const [currentFilter, setCurrentFilter] = React.useState<SearchFilterOption | null>(null);

		const inputRef = React.useRef<HTMLInputElement | null>(null);
		const [suppressAutoOpen, setSuppressAutoOpen] = React.useState(false);
		const suppressAutoOpenRef = React.useRef(false);
		const hintsRef = React.useRef<SearchHints>({usersByTag: {}, channelsByName: {}});

		const searchContextRef = React.useRef<SearchContext | null>(null);
		const [memberSearchResults, setMemberSearchResults] = React.useState<Array<UserRecord>>([]);

		const memberFetchDebounceTimerRef = React.useRef<NodeJS.Timeout | null>(null);
		const memberFetchQueryRef = React.useRef<string>('');

		const selectedGuildId = SelectedGuildStore.selectedGuildId;

		const channelGuildId = channel?.guildId ?? undefined;
		const isInGuildChannel = Boolean(channelGuildId);

		const currentGuildIdForScope = channelGuildId ?? routeGuildId ?? selectedGuildId ?? undefined;

		const filterOptions = React.useMemo(() => [...getSearchFilterOptions(i18n)], [i18n]);

		const contextId = React.useMemo(
			() => getChannelSearchContextId(channel ?? null, selectedGuildId),
			[channel?.guildId, channel?.id, selectedGuildId],
		);

		const searchContext = contextId ? ChannelSearchStore.getContext(contextId) : null;
		const activeScope = searchContext?.scope ?? DEFAULT_SCOPE_VALUE;

		const scopeOptions = React.useMemo(
			() => getScopeOptionsForChannel(i18n, channel),
			[i18n, channel?.id, channel?.type, channel?.guildId],
		);
		const scopeOptionValues = React.useMemo(() => new Set(scopeOptions.map((option) => option.value)), [scopeOptions]);

		React.useEffect(() => {
			if (!scopeOptions.length || !contextId) {
				return;
			}

			const fallbackScope = scopeOptions[0].value;
			const currentScope: MessageSearchScope = activeScope ?? fallbackScope;

			if (!scopeOptionValues.has(currentScope)) {
				ChannelSearchStore.setScope(contextId, fallbackScope);
			}
		}, [scopeOptions, scopeOptionValues, activeScope, contextId]);

		const handleScopeSelect = React.useCallback(
			(scope: MessageSearchScope) => {
				if (!contextId) {
					return;
				}
				ChannelSearchStore.setScope(contextId, scope);
			},
			[contextId],
		);

		const handleScopeMenuOpen = React.useCallback(
			(event: React.MouseEvent<HTMLButtonElement>) => {
				ContextMenuActionCreators.openFromElementBottomRight(event, ({onClose}) => (
					<ContextMenuCloseProvider value={onClose}>
						<MenuGroup>
							{scopeOptions.map((option) => (
								<MenuItemRadio
									key={option.value}
									selected={activeScope === option.value}
									closeOnSelect
									onSelect={() => handleScopeSelect(option.value)}
									icon={React.createElement(SCOPE_ICON_COMPONENTS[option.value] ?? HashIcon, {
										size: 16,
										weight: 'bold',
									})}
								>
									{option.label}
								</MenuItemRadio>
							))}
						</MenuGroup>
					</ContextMenuCloseProvider>
				));
			},
			[handleScopeSelect, scopeOptions, activeScope],
		);

		const activeScopeOption = React.useMemo(() => {
			if (!scopeOptions.length) {
				return null;
			}
			return scopeOptions.find((opt) => opt.value === activeScope) ?? scopeOptions[0];
		}, [scopeOptions, activeScope]);

		const ScopeIconComponent = React.useMemo(() => {
			if (!activeScope) {
				return HashIcon;
			}
			return SCOPE_ICON_COMPONENTS[activeScope] ?? HashIcon;
		}, [activeScope]);

		const scopeTooltipText = React.useMemo(() => {
			if (activeScopeOption?.label) {
				return i18n._(msg`Search scope: ${activeScopeOption.label}`);
			}
			return i18n._(msg`Search scope`);
		}, [i18n, activeScopeOption?.label]);

		React.useEffect(() => {
			const handleGlobalKeydown = (event: KeyboardEvent) => {
				const isFind = (event.key === 'f' || event.key === 'F') && (event.metaKey || event.ctrlKey);
				if (!isFind) return;

				event.preventDefault();
				event.stopPropagation();

				const el = inputRef.current;
				if (!el) return;

				el.focus();
				const pos = el.value.length;
				try {
					el.setSelectionRange(pos, pos);
				} catch {}
			};

			document.addEventListener('keydown', handleGlobalKeydown, true);
			return () => document.removeEventListener('keydown', handleGlobalKeydown, true);
		}, []);

		React.useEffect(() => {
			suppressAutoOpenRef.current = suppressAutoOpen;
		}, [suppressAutoOpen]);

		React.useEffect(() => {
			const context = MemberSearchStore.getSearchContext((results) => {
				const users = results.map((result) => UserStore.getUser(result.id)).filter((u): u is UserRecord => u !== null);
				setMemberSearchResults(users);
			}, 25);

			searchContextRef.current = context;

			return () => {
				context.destroy();
				searchContextRef.current = null;
			};
		}, []);

		React.useEffect(() => {
			if (memberFetchDebounceTimerRef.current) {
				clearTimeout(memberFetchDebounceTimerRef.current);
				memberFetchDebounceTimerRef.current = null;
			}

			if (autocompleteType !== 'users' || !currentFilter || !_isUserFilterKey(currentFilter.key)) {
				memberFetchQueryRef.current = '';
				const context = searchContextRef.current;
				if (context) {
					context.clearQuery();
				}
				setMemberSearchResults([]);
				return;
			}

			const plan = getUserGuildSearchPlan(activeScope, currentGuildIdForScope);

			if (plan.mode === 'none') {
				memberFetchQueryRef.current = '';
				const context = searchContextRef.current;
				if (context) {
					context.clearQuery();
				}
				setMemberSearchResults([]);
				return;
			}

			const cursorPos = inputRef.current?.selectionStart ?? value.length;
			const textBeforeCursor = value.slice(0, cursorPos);
			const words = textBeforeCursor.split(/\s+/);
			const currentWord = words[words.length - 1] || '';
			const searchQuery = currentWord.slice(currentFilter.syntax.length).trim();

			const context = searchContextRef.current;

			if (searchQuery.length === 0) {
				memberFetchQueryRef.current = '';
				if (context) {
					context.clearQuery();
				}
				setMemberSearchResults([]);
				return;
			}

			const fallbackGuildId = currentGuildIdForScope;
			if (fallbackGuildId) {
				const cachedMembers = _deduplicateMembers(GuildMemberStore.getMembers(fallbackGuildId));
				if (cachedMembers.length > 0) {
					const localResults = matchSorter(cachedMembers, searchQuery, {
						keys: [
							(member) => NicknameUtils.getNickname(member.user, fallbackGuildId),
							(member) => member.user.username,
							(member) => member.user.tag,
						],
					})
						.slice(0, 12)
						.map((m) => m.user);

					setMemberSearchResults(localResults);
				} else {
					setMemberSearchResults([]);
				}
			}

			const boosters = buildUserSearchBoosters(channel, currentGuildIdForScope, plan.mode);
			if (context) {
				context.setQuery(searchQuery, plan.workerFilters, new Set(), new Set(), boosters);
			}

			if (!plan.guildsToSearch || plan.guildsToSearch.length === 0) {
				memberFetchQueryRef.current = searchQuery;
				return;
			}

			memberFetchQueryRef.current = searchQuery;
			const scheduledQuery = searchQuery;

			memberFetchDebounceTimerRef.current = setTimeout(() => {
				memberFetchDebounceTimerRef.current = null;

				if (autocompleteType !== 'users' || !currentFilter || !_isUserFilterKey(currentFilter.key)) {
					return;
				}

				if (memberFetchQueryRef.current !== scheduledQuery) {
					return;
				}

				const guildIds = plan.guildsToSearch?.map((g) => g.id) ?? [];

				const priorityGuildId = plan.priorityGuildId;

				void MemberSearchStore.fetchMembersInBackground(scheduledQuery, guildIds, priorityGuildId);
			}, 300);

			return () => {
				if (memberFetchDebounceTimerRef.current) {
					clearTimeout(memberFetchDebounceTimerRef.current);
					memberFetchDebounceTimerRef.current = null;
				}
			};
		}, [autocompleteType, currentFilter, value, activeScope, channel, currentGuildIdForScope]);

		React.useEffect(() => {
			if (autocompleteType !== 'users' || !currentFilter) {
				const context = searchContextRef.current;
				if (context) {
					context.clearQuery();
				}
				setMemberSearchResults([]);
				memberFetchQueryRef.current = '';
				if (memberFetchDebounceTimerRef.current) {
					clearTimeout(memberFetchDebounceTimerRef.current);
					memberFetchDebounceTimerRef.current = null;
				}
			}
		}, [autocompleteType, currentFilter]);

		const listboxId = React.useMemo(() => `message-search-listbox-${channel?.id ?? 'global'}`, [channel?.id]);

		const {refs, floatingStyles, isPositioned} = useFloating({
			placement: 'bottom-start',
			open: isFocused && autocompleteType !== null,
			whileElementsMounted: autoUpdate,
			middleware: [
				offset(8),
				flip({padding: 16}),
				size({
					apply({rects, elements}) {
						const minWidth = 380;
						const maxWidth = Math.min(window.innerWidth - 32, 480);
						const width = Math.min(maxWidth, Math.max(rects.reference.width, minWidth));
						Object.assign(elements.floating.style, {
							width: `${width}px`,
						});
					},
					padding: 16,
				}),
			],
		});

		const getAutocompleteTypeForFilter = React.useCallback(
			(filter: SearchFilterOption): AutocompleteType => {
				const keyBase = normalizeFilterKey(filter.key);

				switch (keyBase) {
					case 'before':
					case 'after':
					case 'during':
					case 'on':
						return 'date';

					case 'from':
					case 'mentions':
						return 'users';

					case 'in':
						return isInGuildChannel ? 'channels' : 'values';

					default:
						return 'values';
				}
			},
			[isInGuildChannel],
		);

		const getAutocompleteOptions = React.useCallback((): Array<AutocompleteOption> => {
			const cursorPos = inputRef.current?.selectionStart ?? value.length;
			const textBeforeCursor = value.slice(0, cursorPos);
			const words = textBeforeCursor.split(/\s+/);
			const currentWord = words[words.length - 1] || '';

			switch (autocompleteType) {
				case 'filters': {
					const filtered = filterOptions.filter((opt) => {
						if (opt.requiresGuild && !isInGuildChannel) return false;

						if (!currentWord) {
							return !opt.key.startsWith('-');
						}

						const currentWordLower = currentWord.toLowerCase();
						if (currentWordLower.startsWith('-')) {
							return (
								(opt.key.startsWith('-') && currentWordLower === '-') ||
								currentWordLower.startsWith(opt.syntax.toLowerCase())
							);
						}

						if (opt.key.startsWith('-')) {
							return false;
						}

						return opt.syntax.toLowerCase().includes(currentWordLower);
					});

					const MAX_TYPED_FILTERS = 15;
					return currentWord ? filtered.slice(0, MAX_TYPED_FILTERS) : filtered;
				}

				case 'history': {
					return SearchHistoryStore.search(currentWord, channel?.id).slice(0, 5);
				}

				case 'users': {
					if (!currentFilter) return [];
					const searchTerm = currentWord.slice(currentFilter.syntax.length);

					const plan = getUserGuildSearchPlan(activeScope, currentGuildIdForScope);

					if (plan.mode !== 'none') {
						if (memberSearchResults.length > 0) {
							return memberSearchResults.slice(0, 12);
						}

						const fallbackGuildId = currentGuildIdForScope;

						if (fallbackGuildId) {
							const isGuildFullyLoaded = GuildMemberStore.isGuildFullyLoaded(fallbackGuildId);
							if (isGuildFullyLoaded) {
								const cachedMembers = GuildMemberStore.getMembers(fallbackGuildId);
								return matchSorter(cachedMembers, searchTerm, {
									keys: [
										(member) => NicknameUtils.getNickname(member.user, fallbackGuildId),
										(member) => member.user.username,
										(member) => member.user.tag,
									],
								})
									.slice(0, 12)
									.map((m) => m.user);
							}
						}

						return [];
					}

					if (channel) {
						const users = channel.recipientIds
							.map((id) => UserStore.getUser(id))
							.filter((u): u is UserRecord => u !== null);

						return matchSorter(users, searchTerm, {keys: ['username', 'tag']}).slice(0, 12);
					}

					return [];
				}

				case 'channels': {
					if (!currentFilter) return [];
					const guildIdForChannels = channelGuildId ?? routeGuildId;
					if (!guildIdForChannels) return [];

					const searchTerm = currentWord.slice(currentFilter.syntax.length);
					const channels = ChannelStore.getGuildChannels(guildIdForChannels).filter(
						(c) => c.type === ChannelTypes.GUILD_TEXT,
					);

					const recentVisitsForGuild = SelectedChannelStore.recentlyVisitedChannels
						.filter((visit) => visit.guildId === guildIdForChannels)
						.sort((a, b) => b.timestamp - a.timestamp);

					const recencyRank = new Map<string, number>();
					recentVisitsForGuild.forEach((visit, index) => {
						if (!recencyRank.has(visit.channelId)) {
							recencyRank.set(visit.channelId, index);
						}
					});

					const currentChannelId = channel?.id;
					const matches = matchSorter(channels, searchTerm, {keys: ['name']});
					const orderedMatches = [...matches].sort((a, b) => {
						const resolveRank = (ch: ChannelRecord) => {
							if (ch.id === currentChannelId) return -1;
							return recencyRank.get(ch.id) ?? Number.MAX_SAFE_INTEGER;
						};

						const rankDifference = resolveRank(a) - resolveRank(b);
						if (rankDifference !== 0) {
							return rankDifference;
						}

						return (a.name ?? '').localeCompare(b.name ?? '');
					});

					return orderedMatches.slice(0, 12);
				}

				case 'values': {
					if (!currentFilter?.values) return [];
					const searchTerm = currentWord.slice(currentFilter.syntax.length);

					const matches = matchSorter(currentFilter.values, searchTerm, {
						keys: ['value', 'label', 'description'],
					});

					const matchValues = new Set(matches.map((option) => option.value));
					return currentFilter.values.filter((option) => matchValues.has(option.value));
				}

				case 'date': {
					const now = DateTime.local();
					const fmtDate = (dt: DateTime) => dt.toFormat('yyyy-MM-dd');
					const fmtDateTime = (dt: DateTime) => dt.toFormat("yyyy-MM-dd'T'HH:mm");

					return [
						{label: i18n._(msg`Today`), value: fmtDate(now)},
						{label: i18n._(msg`Yesterday`), value: fmtDate(now.minus({days: 1}))},
						{label: i18n._(msg`Now`), value: fmtDateTime(now)},
					];
				}

				default:
					return [];
			}
		}, [
			autocompleteType,
			value,
			filterOptions,
			isInGuildChannel,
			currentFilter,
			channelGuildId,
			routeGuildId,
			channel,
			memberSearchResults,
			i18n,
			activeScope,
			currentGuildIdForScope,
		]);

		const getHistoryCommonFilters = React.useCallback(() => {
			return filterOptions
				.filter((opt) => !opt.requiresGuild || isInGuildChannel)
				.filter((opt) => !opt.key.startsWith('-'));
		}, [filterOptions, isInGuildChannel]);

		const getTotalOptions = React.useCallback((): number => {
			if (!autocompleteType) return 0;

			if (autocompleteType === 'history') {
				return getHistoryCommonFilters().length + getAutocompleteOptions().length;
			}

			return getAutocompleteOptions().length;
		}, [autocompleteType, getAutocompleteOptions, getHistoryCommonFilters]);

		const hasAnyOptions = React.useCallback((): boolean => {
			return getTotalOptions() > 0;
		}, [getTotalOptions]);

		const getSelectedOption = React.useCallback((): AutocompleteOption | null => {
			if (selectedIndex < 0) return null;

			if (autocompleteType === 'history') {
				const commonFilters = getHistoryCommonFilters();
				if (selectedIndex < commonFilters.length) {
					return commonFilters[selectedIndex] ?? null;
				}

				const historyOptions = getAutocompleteOptions();
				const historyIndex = selectedIndex - commonFilters.length;
				return historyOptions[historyIndex] ?? null;
			}

			const options = getAutocompleteOptions();
			return options[selectedIndex] ?? null;
		}, [selectedIndex, autocompleteType, getAutocompleteOptions, getHistoryCommonFilters]);

		React.useEffect(() => {
			if (!isFocused || suppressAutoOpen) {
				setAutocompleteType(null);
				setCurrentFilter(null);
				setSelectedIndex(-1);
				setHoverIndex(-1);
				setHasNavigated(false);
				setHasInteracted(false);
				return;
			}

			const cursorPos = inputRef.current?.selectionStart ?? value.length;
			const textBeforeCursor = value.slice(0, cursorPos);
			const words = textBeforeCursor.split(/\s+/);
			const currentWord = words[words.length - 1] || '';

			const matchingFilter = filterOptions.find((opt) => currentWord.startsWith(opt.syntax));

			if (matchingFilter) {
				const afterColon = currentWord.slice(matchingFilter.syntax.length);
				const filterKeyBase = normalizeFilterKey(matchingFilter.key);

				if (matchingFilter.requiresGuild && !isInGuildChannel) {
					setAutocompleteType(null);
					setCurrentFilter(null);
					return;
				}

				if (isDateFilterKey(filterKeyBase)) {
					setAutocompleteType('date');
					setCurrentFilter(matchingFilter);
					return;
				}

				if (matchingFilter.values && afterColon.length === 0) {
					setAutocompleteType('values');
					setCurrentFilter(matchingFilter);
					return;
				}

				if (filterKeyBase === 'from' || filterKeyBase === 'mentions') {
					setAutocompleteType('users');
					setCurrentFilter(matchingFilter);
					setSelectedIndex(0);
					setHasNavigated(false);
					return;
				}

				if (filterKeyBase === 'in' && isInGuildChannel) {
					setAutocompleteType('channels');
					setCurrentFilter(matchingFilter);
					setSelectedIndex(0);
					setHasNavigated(false);
					return;
				}

				if (matchingFilter.values) {
					setAutocompleteType('values');
					setCurrentFilter(matchingFilter);
					return;
				}

				setAutocompleteType(null);
				setCurrentFilter(null);
				return;
			}

			if (currentWord === '') {
				setAutocompleteType('history');
				setCurrentFilter(null);
				return;
			}

			const partialMatch = filterOptions.some((opt) => {
				return opt.syntax.includes(currentWord) || currentWord.includes(opt.key) || opt.key.includes(currentWord);
			});

			setAutocompleteType(partialMatch ? 'filters' : null);
			setCurrentFilter(null);
		}, [value, isFocused, isInGuildChannel, suppressAutoOpen, filterOptions]);

		React.useEffect(() => {
			const totalOptions = getTotalOptions();
			if (totalOptions > 0 && (selectedIndex >= totalOptions || selectedIndex < -1)) {
				setSelectedIndex(-1);
			}
		}, [autocompleteType, selectedIndex, getTotalOptions]);

		const setInputRefs = (node: HTMLInputElement | null) => {
			inputRef.current = node;
			assignRef(inputRefExternal, node);
		};

		const handleOptionMouseEnter = (index: number) => {
			setHoverIndex(index);
			setHasInteracted(true);
		};

		const handleOptionMouseLeave = () => {
			setHoverIndex(-1);
		};

		const shouldShowKeyboardFocus = hasNavigated || autocompleteType === 'users' || autocompleteType === 'channels';
		const shouldShowHover = hasInteracted;
		const keyboardFocusIndex = shouldShowKeyboardFocus ? selectedIndex : -1;
		const hoverIndexForRender = shouldShowHover ? hoverIndex : -1;

		const getAriaActiveDescendant = React.useCallback((): string | undefined => {
			if (!isFocused || autocompleteType === null) return undefined;

			const totalOptions = getTotalOptions();
			if (totalOptions <= 0) return undefined;

			const showFocus = shouldShowKeyboardFocus || shouldShowHover;
			if (!showFocus) return undefined;

			if (selectedIndex < 0) return undefined;

			return `${listboxId}-opt-${selectedIndex}`;
		}, [
			isFocused,
			autocompleteType,
			getTotalOptions,
			shouldShowKeyboardFocus,
			shouldShowHover,
			selectedIndex,
			listboxId,
		]);

		const handleAutocompleteSelect = (option: AutocompleteOption) => {
			const cursorPos = inputRef.current?.selectionStart ?? value.length;
			const textBeforeCursor = value.slice(0, cursorPos);
			const textAfterCursor = value.slice(cursorPos);
			const words = textBeforeCursor.split(/\s+/);
			const currentWord = words[words.length - 1] || '';
			const lastWordStart = textBeforeCursor.length - currentWord.length;

			let newText = '';
			let newCursorPos = 0;
			let shouldSubmit = false;

			const insertToken = (syntax: string, tokenValue: string, addSpaceAfter = true) => {
				const needsQuotes = /\s/.test(tokenValue);
				const display = needsQuotes ? `${syntax}"${tokenValue}"` : `${syntax}${tokenValue}`;
				const before = textBeforeCursor.slice(0, lastWordStart);
				const space = addSpaceAfter ? ' ' : '';
				newText = `${before}${display}${space}${textAfterCursor}`;
				newCursorPos = (before + display).length + space.length;
			};

			switch (autocompleteType) {
				case 'filters': {
					const filter = option as SearchFilterOption;
					const requiresValue = filterRequiresValue(filter);
					insertToken(filter.syntax, '', !requiresValue);
					shouldSubmit = !requiresValue;
					break;
				}

				case 'users': {
					const user = option as UserRecord;
					const tag = `${user.username}#${user.discriminator}`;
					insertToken(currentFilter!.syntax, tag);
					hintsRef.current.usersByTag[tag] = user.id;
					shouldSubmit = true;
					break;
				}

				case 'channels': {
					const ch = option as ChannelRecord;
					const name = ch.name || i18n._(msg`Unnamed`);
					insertToken(currentFilter!.syntax, name);
					hintsRef.current.channelsByName[name] = ch.id;
					shouldSubmit = true;
					break;
				}

				case 'values': {
					const valueOption = option as {value: string; label: string};
					const before = textBeforeCursor.slice(0, lastWordStart);
					const display = `${currentFilter!.syntax}${valueOption.value}`;
					newText = `${before}${display} ${textAfterCursor}`;
					newCursorPos = (before + display).length + 1;
					shouldSubmit = true;
					break;
				}

				case 'date': {
					const dateOption = option as {value: string; label: string};
					const before = textBeforeCursor.slice(0, lastWordStart);
					const display = `${currentFilter!.syntax}${dateOption.value}`;
					newText = `${before}${display} ${textAfterCursor}`;
					newCursorPos = (before + display).length + 1;
					shouldSubmit = true;
					break;
				}

				case 'history': {
					const entry = option as SearchHistoryEntry;
					newText = entry.query;
					newCursorPos = newText.length;

					const segments: Array<SearchSegment> = [];
					if (entry.hints?.usersByTag) {
						for (const [tag, id] of Object.entries(entry.hints.usersByTag)) {
							segments.push({type: 'user', filterKey: 'from', id, displayText: `from:${tag}`, start: 0, end: 0});
							segments.push({
								type: 'user',
								filterKey: 'mentions',
								id,
								displayText: `mentions:${tag}`,
								start: 0,
								end: 0,
							});
						}
					}

					if (entry.hints?.channelsByName) {
						for (const [name, id] of Object.entries(entry.hints.channelsByName)) {
							const disp = /\s/.test(name) ? `in:"${name}"` : `in:${name}`;
							segments.push({type: 'channel', filterKey: 'in', id, displayText: disp, start: 0, end: 0});
						}
					}

					onChange(newText, segments);
					SearchHistoryStore.add(newText, channel?.id, entry.hints);

					setTimeout(() => {
						inputRef.current?.focus();
						inputRef.current?.setSelectionRange(newCursorPos, newCursorPos);
					}, 0);

					setSelectedIndex(-1);
					setAutocompleteType(null);
					setCurrentFilter(null);
					setSuppressAutoOpen(true);
					setTimeout(() => onSearch(), 0);
					return;
				}

				default:
					return;
			}

			onChange(newText, []);

			setTimeout(() => {
				inputRef.current?.focus();
				inputRef.current?.setSelectionRange(newCursorPos, newCursorPos);
			}, 0);

			setSelectedIndex(-1);
			setAutocompleteType(null);
			setCurrentFilter(null);

			if (shouldSubmit && newText.trim().length > 0) {
				SearchHistoryStore.add(newText, channel?.id, hintsRef.current);
				setSuppressAutoOpen(true);
				setTimeout(() => onSearch(), 0);
			}
		};

		const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
			if (e.key === '?' && autocompleteType === null) {
				e.preventDefault();
				return;
			}

			if (e.key === 'Enter' && autocompleteType === null) {
				e.preventDefault();
				SearchHistoryStore.add(value, channel?.id, hintsRef.current);
				setAutocompleteType(null);
				setCurrentFilter(null);
				setHasNavigated(false);
				setSuppressAutoOpen(true);
				suppressAutoOpenRef.current = true;
				onSearch();
				return;
			}

			if (e.key === 'Escape') {
				e.preventDefault();

				if (value.trim().length > 0) {
					onChange('', []);
					setSelectedIndex(-1);
					setHasNavigated(false);
					setSuppressAutoOpen(false);
					return;
				}

				if (isResultsOpen) {
					setAutocompleteType(null);
					setCurrentFilter(null);
					return;
				}

				setAutocompleteType(null);
				setCurrentFilter(null);
				inputRef.current?.blur();
				return;
			}

			if (!autocompleteType) {
				return;
			}

			const totalOptions = getTotalOptions();
			if (totalOptions <= 0) {
				return;
			}

			switch (e.key) {
				case 'ArrowDown': {
					e.preventDefault();
					setSelectedIndex((prev) => (prev + 1) % totalOptions);
					setHasNavigated(true);
					return;
				}

				case 'ArrowUp': {
					e.preventDefault();
					setSelectedIndex((prev) => {
						if (prev === -1) return totalOptions - 1;
						return (prev - 1 + totalOptions) % totalOptions;
					});
					setHasNavigated(true);
					return;
				}

				case 'Home': {
					e.preventDefault();
					setSelectedIndex(0);
					setHasNavigated(true);
					return;
				}

				case 'End': {
					e.preventDefault();
					setSelectedIndex(totalOptions - 1);
					setHasNavigated(true);
					return;
				}

				case 'Enter':
				case 'Tab': {
					e.preventDefault();

					let shouldAutoSelect = hasNavigated;
					if (autocompleteType === 'users' || autocompleteType === 'channels') {
						shouldAutoSelect = true;
					}

					const cursorPos = inputRef.current?.selectionStart ?? value.length;
					const textBeforeCursor = value.slice(0, cursorPos);
					const words = textBeforeCursor.split(/\s+/);
					const currentWord = words[words.length - 1] || '';
					const matchingFilter = filterOptions.find((opt) => currentWord.startsWith(opt.syntax));
					const afterColon = matchingFilter ? currentWord.slice(matchingFilter.syntax.length) : '';

					if (
						e.key === 'Enter' &&
						autocompleteType === 'users' &&
						matchingFilter &&
						USER_FILTER_KEYS.has(matchingFilter.key) &&
						afterColon.length > 0 &&
						getAutocompleteOptions().length === 0
					) {
						SearchHistoryStore.add(value, channel?.id, hintsRef.current);
						setAutocompleteType(null);
						setCurrentFilter(null);
						setHasNavigated(false);
						setSuppressAutoOpen(true);
						suppressAutoOpenRef.current = true;
						onSearch();
						return;
					}

					if (matchingFilter) {
						const requiresValue = filterRequiresValue(matchingFilter);
						if (!shouldAutoSelect && requiresValue && afterColon.length === 0) {
							return;
						}
					}

					if (!shouldAutoSelect) {
						SearchHistoryStore.add(value, channel?.id, hintsRef.current);
						setAutocompleteType(null);
						setCurrentFilter(null);
						setHasNavigated(false);
						setSuppressAutoOpen(true);
						suppressAutoOpenRef.current = true;
						setTimeout(() => onSearch(), 0);
						return;
					}

					const selected = getSelectedOption();
					if (selected) {
						const isFilterOptionInHistory =
							autocompleteType === 'history' && typeof selected === 'object' && selected !== null && 'key' in selected;

						if (isFilterOptionInHistory) {
							const filter = selected as SearchFilterOption;

							const cursorPosInner = inputRef.current?.selectionStart ?? value.length;
							const textBeforeCursorInner = value.slice(0, cursorPosInner);
							const textAfterCursorInner = value.slice(cursorPosInner);

							const wordsInner = textBeforeCursorInner.split(/\s+/);
							const currentWordInner = wordsInner[wordsInner.length - 1] || '';
							const lastWordStartInner = textBeforeCursorInner.length - currentWordInner.length;

							const display = filter.syntax;
							const before = textBeforeCursorInner.slice(0, lastWordStartInner);
							const requiresValue = filterRequiresValue(filter);
							const space = requiresValue ? '' : ' ';
							const newText = `${before}${display}${space}${textAfterCursorInner}`;
							const newCursorPos = (before + display).length + space.length;

							onChange(newText, []);

							setTimeout(() => {
								inputRef.current?.setSelectionRange(newCursorPos, newCursorPos);
							}, 0);

							if (!requiresValue) {
								setTimeout(() => {
									SearchHistoryStore.add(newText, channel?.id, hintsRef.current);
									setSuppressAutoOpen(true);
									setTimeout(() => onSearch(), 0);
									setAutocompleteType(null);
									setCurrentFilter(null);
								}, 10);
								return;
							}

							setCurrentFilter(filter);
							setAutocompleteType(getAutocompleteTypeForFilter(filter));
							setSelectedIndex(-1);
							setHasNavigated(false);
							return;
						}

						handleAutocompleteSelect(selected);
					}

					return;
				}

				default:
					return;
			}
		};

		const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
			onChange(e.target.value, []);
			setHasNavigated(false);
			setSuppressAutoOpen(false);
			setHasInteracted(false);
		};

		const handleHistoryClear = () => {
			SearchHistoryStore.clear(channel?.id);
			setAutocompleteType('filters');
			setSelectedIndex(-1);
		};

		const handleFilterSelect = (filter: SearchFilterOption, index: number) => {
			setSelectedIndex(index);

			const cursorPos = inputRef.current?.selectionStart ?? value.length;
			const textBeforeCursor = value.slice(0, cursorPos);
			const textAfterCursor = value.slice(cursorPos);

			const words = textBeforeCursor.split(/\s+/);
			const currentWord = words[words.length - 1] || '';
			const lastWordStart = textBeforeCursor.length - currentWord.length;

			const display = filter.syntax;
			const before = textBeforeCursor.slice(0, lastWordStart);
			const requiresValue = filterRequiresValue(filter);
			const space = requiresValue ? '' : ' ';
			const newText = `${before}${display}${space}${textAfterCursor}`;
			const newCursorPos = (before + display).length + space.length;

			onChange(newText, []);

			setTimeout(() => {
				inputRef.current?.setSelectionRange(newCursorPos, newCursorPos);
			}, 0);

			if (!requiresValue) {
				setTimeout(() => {
					SearchHistoryStore.add(newText, channel?.id, hintsRef.current);
					setSuppressAutoOpen(true);
					setTimeout(() => onSearch(), 0);
					setAutocompleteType(null);
					setCurrentFilter(null);
				}, 10);
				return;
			}

			setCurrentFilter(filter);
			setAutocompleteType(getAutocompleteTypeForFilter(filter));
		};

		const renderAutocompleteContent = () => {
			switch (autocompleteType) {
				case 'filters':
					return (
						<FiltersSection
							options={getAutocompleteOptions() as Array<SearchFilterOption>}
							selectedIndex={keyboardFocusIndex}
							hoverIndex={hoverIndexForRender}
							onSelect={handleAutocompleteSelect}
							onMouseEnter={handleOptionMouseEnter}
							onMouseLeave={handleOptionMouseLeave}
							listboxId={listboxId}
						/>
					);

				case 'history':
					return (
						<HistorySection
							selectedIndex={keyboardFocusIndex}
							hoverIndex={hoverIndexForRender}
							onSelect={handleAutocompleteSelect}
							onMouseEnter={handleOptionMouseEnter}
							onMouseLeave={handleOptionMouseLeave}
							listboxId={listboxId}
							isInGuild={isInGuildChannel}
							channelId={channel?.id}
							onHistoryClear={handleHistoryClear}
							onFilterSelect={handleFilterSelect}
							onFilterMouseEnter={(index) => {
								setHoverIndex(index);
								setHasInteracted(true);
							}}
							onFilterMouseLeave={handleOptionMouseLeave}
							filterOptions={filterOptions}
						/>
					);

				case 'users':
					return (
						<UsersSection
							options={getAutocompleteOptions() as Array<UserRecord>}
							selectedIndex={keyboardFocusIndex}
							hoverIndex={hoverIndexForRender}
							onSelect={handleAutocompleteSelect}
							onMouseEnter={handleOptionMouseEnter}
							onMouseLeave={handleOptionMouseLeave}
							listboxId={listboxId}
							guildId={currentGuildIdForScope}
							isInGuild={isInGuildChannel}
						/>
					);

				case 'channels':
					return (
						<ChannelsSection
							options={getAutocompleteOptions() as Array<ChannelRecord>}
							selectedIndex={keyboardFocusIndex}
							hoverIndex={hoverIndexForRender}
							onSelect={handleAutocompleteSelect}
							onMouseEnter={handleOptionMouseEnter}
							onMouseLeave={handleOptionMouseLeave}
							listboxId={listboxId}
						/>
					);

				case 'values':
					return (
						<ValuesSection
							options={getAutocompleteOptions() as Array<{value: string; label: string}>}
							selectedIndex={keyboardFocusIndex}
							hoverIndex={hoverIndexForRender}
							onSelect={handleAutocompleteSelect}
							onMouseEnter={handleOptionMouseEnter}
							onMouseLeave={handleOptionMouseLeave}
							listboxId={listboxId}
						/>
					);

				case 'date':
					return (
						<DateSection
							selectedIndex={keyboardFocusIndex}
							hoverIndex={hoverIndexForRender}
							onSelect={handleAutocompleteSelect}
							onMouseEnter={handleOptionMouseEnter}
							onMouseLeave={handleOptionMouseLeave}
							listboxId={listboxId}
						/>
					);

				default:
					return null;
			}
		};

		const hasValue = value.length > 0;
		const ariaActiveDescendant = getAriaActiveDescendant();

		return (
			<>
				<div ref={refs.setReference} className={styles.anchor}>
					<div className={styles.inputContainer}>
						<Tooltip text={scopeTooltipText} position="bottom">
							<button
								type="button"
								className={styles.scopeButton}
								onClick={handleScopeMenuOpen}
								aria-label={scopeTooltipText}
							>
								<MagnifyingGlassIcon className={styles.searchIcon} weight="bold" />
								<span className={styles.scopeBadge}>
									<ScopeIconComponent size={8} weight="bold" />
								</span>
							</button>
						</Tooltip>

						<input
							ref={setInputRefs}
							type="text"
							value={value}
							onChange={handleInputChange}
							onMouseDown={() => setSuppressAutoOpen(false)}
							onKeyDown={handleKeyDown}
							onFocus={() => {
								setIsFocused(true);
								if (KeyboardModeStore.keyboardModeEnabled) {
									setSuppressAutoOpen(false);
								}
							}}
							onBlur={() => {
								setIsFocused(false);
								if (isResultsOpen && value.trim().length === 0) {
									onCloseResults?.();
								}
							}}
							role="combobox"
							aria-autocomplete="list"
							aria-expanded={isFocused && autocompleteType !== null}
							aria-controls={isFocused && autocompleteType !== null ? listboxId : undefined}
							aria-activedescendant={ariaActiveDescendant}
							placeholder={i18n._(msg`Search messages`)}
							className={styles.input}
						/>

						{hasValue && (
							<button
								type="button"
								onMouseDown={(ev) => ev.preventDefault()}
								onClick={() => {
									onClear();
									setSelectedIndex(-1);
									inputRef.current?.focus();
								}}
								className={styles.clearButton}
								aria-label={i18n._(msg`Clear search`)}
							>
								<XIcon weight="bold" className={styles.optionMetaIcon} />
							</button>
						)}
					</div>
				</div>

				{isFocused && autocompleteType && hasAnyOptions() && (
					<FloatingPortal>
						{/* biome-ignore lint/a11y/noStaticElementInteractions: This wrapper intercepts mousedown to preserve input focus for the listbox. */}
						<div
							ref={refs.setFloating}
							style={{...floatingStyles, visibility: isPositioned ? 'visible' : 'hidden'}}
							className={styles.popoutContainer}
							onMouseDown={(e) => {
								if (e.button === 0) e.preventDefault();
							}}
						>
							<div className={styles.popoutInner}>
								<div className={`${styles.flex} ${styles.flexCol}`}>
									<div
										id={listboxId}
										role="listbox"
										aria-label={i18n._(msg`Search suggestions`)}
										className={styles.list}
									>
										{renderAutocompleteContent()}
									</div>
								</div>
							</div>
						</div>
					</FloatingPortal>
				)}
			</>
		);
	},
);
