"use client";

import {
	Combobox,
	ComboboxGroup,
	ComboboxGroupLabel,
	ComboboxItem,
	type ComboboxItemProps,
	ComboboxPopover,
	ComboboxProvider,
	ComboboxRow,
	Portal,
	useComboboxContext,
	useComboboxStore,
} from "@ariakit/react";
import { filterWords } from "@platejs/combobox";
import {
	type UseComboboxInputResult,
	useComboboxInput,
	useHTMLInputCursorState,
} from "@platejs/combobox/react";
import { cva } from "class-variance-authority";
import type { Point, TElement } from "platejs";
import { useComposedRef, useEditorRef } from "platejs/react";
import * as React from "react";

import { cn } from "@/lib/utils";

function useRequiredComboboxContext() {
	const context = useComboboxContext();

	if (!context) {
		throw new Error("InlineCombobox compound components must be rendered within InlineCombobox");
	}

	return context;
}

type FilterFn = (
	item: { value: string; group?: string; keywords?: string[]; label?: string },
	search: string
) => boolean;

type InlineComboboxContextValue = {
	filter: FilterFn | false;
	inputProps: UseComboboxInputResult["props"];
	inputRef: React.RefObject<HTMLInputElement | null>;
	removeInput: UseComboboxInputResult["removeInput"];
	showTrigger: boolean;
	trigger: string;
	setHasEmpty: (hasEmpty: boolean) => void;
};

const InlineComboboxContext = React.createContext<InlineComboboxContextValue>(
	null as unknown as InlineComboboxContextValue
);

const defaultFilter: FilterFn = ({ group, keywords = [], label, value }, search) => {
	const uniqueTerms = new Set([value, ...keywords, group, label].filter(Boolean));

	return Array.from(uniqueTerms).some((keyword) => filterWords(keyword as string, search));
};

type InlineComboboxProps = {
	children: React.ReactNode;
	element: TElement;
	trigger: string;
	filter?: FilterFn | false;
	hideWhenNoValue?: boolean;
	showTrigger?: boolean;
	value?: string;
	setValue?: (value: string) => void;
};

const InlineCombobox = ({
	children,
	element,
	filter = defaultFilter,
	hideWhenNoValue = false,
	setValue: setValueProp,
	showTrigger = true,
	trigger,
	value: valueProp,
}: InlineComboboxProps) => {
	const editor = useEditorRef();
	const inputRef = React.useRef<HTMLInputElement>(null);
	const cursorState = useHTMLInputCursorState(inputRef);

	const [valueState, setValueState] = React.useState("");
	const hasValueProp = valueProp !== undefined;
	const value = hasValueProp ? valueProp : valueState;

	// Check if current user is the creator of this element (for Yjs collaboration)
	const isCreator = React.useMemo(() => {
		const elementUserId = (element as Record<string, unknown>).userId;
		const currentUserId = editor.meta.userId;

		// If no userId (backwards compatibility or non-Yjs), allow
		if (!elementUserId) return true;

		return elementUserId === currentUserId;
	}, [editor.meta.userId, element]);

	const setValue = React.useCallback(
		(newValue: string) => {
			setValueProp?.(newValue);

			if (!hasValueProp) {
				setValueState(newValue);
			}
		},
		[setValueProp, hasValueProp]
	);

	/**
	 * Track the point just before the input element so we know where to
	 * insertText if the combobox closes due to a selection change.
	 */
	const insertPoint = React.useRef<Point | null>(null);

	React.useEffect(() => {
		const path = editor.api.findPath(element);

		if (!path) return;

		const point = editor.api.before(path);

		if (!point) return;

		const pointRef = editor.api.pointRef(point);
		insertPoint.current = pointRef.current;

		return () => {
			pointRef.unref();
		};
	}, [editor, element]);

	const { props: inputProps, removeInput } = useComboboxInput({
		cancelInputOnBlur: true,
		cursorState,
		autoFocus: isCreator,
		ref: inputRef,
		onCancelInput: (cause) => {
			if (cause !== "backspace") {
				editor.tf.insertText(trigger + value, {
					at: insertPoint?.current ?? undefined,
				});
			}
			if (cause === "arrowLeft" || cause === "arrowRight") {
				editor.tf.move({
					distance: 1,
					reverse: cause === "arrowLeft",
				});
			}
		},
	});

	const [hasEmpty, setHasEmpty] = React.useState(false);

	const contextValue: InlineComboboxContextValue = React.useMemo(
		() => ({
			filter,
			inputProps,
			inputRef,
			removeInput,
			setHasEmpty,
			showTrigger,
			trigger,
		}),
		[trigger, showTrigger, filter, inputProps, removeInput]
	);

	const store = useComboboxStore({
		// open: ,
		setValue: (newValue) => React.startTransition(() => setValue(newValue)),
	});

	const items = store.useState("items");

	/**
	 * If there is no active ID and the list of items changes, select the first
	 * item.
	 */
	React.useEffect(() => {
		if (items.length === 0) return;

		if (!store.getState().activeId) {
			store.setActiveId(store.first());
		}
	}, [items, store]);

	return (
		<span contentEditable={false}>
			<ComboboxProvider
				open={(items.length > 0 || hasEmpty) && (!hideWhenNoValue || value.length > 0)}
				store={store}
			>
				<InlineComboboxContext.Provider value={contextValue}>
					{children}
				</InlineComboboxContext.Provider>
			</ComboboxProvider>
		</span>
	);
};

const InlineComboboxInput = ({
	className,
	ref: propRef,
	...props
}: React.HTMLAttributes<HTMLInputElement> & {
	ref?: React.RefObject<HTMLInputElement | null>;
}) => {
	const {
		inputProps,
		inputRef: contextRef,
		showTrigger,
		trigger,
	} = React.useContext(InlineComboboxContext);

	const store = useRequiredComboboxContext();
	const value = store.useState("value");

	const ref = useComposedRef(propRef, contextRef);

	/**
	 * To create an auto-resizing input, we render a visually hidden span
	 * containing the input value and position the input element on top of it.
	 * This works well for all cases except when input exceeds the width of the
	 * container.
	 */

	return (
		<>
			{showTrigger && trigger}

			<span className="relative min-h-[1lh]">
				<span className="invisible overflow-hidden text-nowrap" aria-hidden="true">
					{value || "\u200B"}
				</span>

				<Combobox
					ref={ref}
					className={cn("absolute top-0 left-0 size-full bg-transparent outline-none", className)}
					value={value}
					autoSelect
					{...inputProps}
					{...props}
				/>
			</span>
		</>
	);
};

InlineComboboxInput.displayName = "InlineComboboxInput";

const InlineComboboxContent: typeof ComboboxPopover = ({ className, ...props }) => {
	// Portal prevents CSS from leaking into popover
	const store = useComboboxContext();

	function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
		if (!store) return;

		const state = store.getState();
		const { items, activeId } = state;

		if (!items.length) return;

		const currentIndex = items.findIndex((item) => item.id === activeId);

		if (event.key === "ArrowUp" && currentIndex <= 0) {
			event.preventDefault();
			store.setActiveId(store.last());
		} else if (event.key === "ArrowDown" && currentIndex >= items.length - 1) {
			event.preventDefault();
			store.setActiveId(store.first());
		}
	}

	return (
		<Portal>
			<ComboboxPopover
				className={cn(
					"z-500 max-h-[288px] w-[300px] overflow-y-auto rounded-md bg-popover shadow-md",
					className
				)}
				onKeyDownCapture={handleKeyDown}
				{...props}
			/>
		</Portal>
	);
};

const comboboxItemVariants = cva(
	"relative mx-1 flex h-[28px] select-none items-center rounded-sm px-2 text-foreground text-sm outline-none [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
	{
		defaultVariants: {
			interactive: true,
		},
		variants: {
			interactive: {
				false: "",
				true: "cursor-pointer transition-colors hover:bg-accent hover:text-accent-foreground data-[active-item=true]:bg-accent data-[active-item=true]:text-accent-foreground dark:hover:bg-neutral-700 dark:data-[active-item=true]:bg-neutral-700",
			},
		},
	}
);

const InlineComboboxItem = ({
	className,
	focusEditor = true,
	group,
	keywords,
	label,
	onClick,
	...props
}: {
	focusEditor?: boolean;
	group?: string;
	keywords?: string[];
	label?: string;
} & ComboboxItemProps &
	Required<Pick<ComboboxItemProps, "value">>) => {
	const { value } = props;

	const { filter, removeInput } = React.useContext(InlineComboboxContext);

	const store = useRequiredComboboxContext();

	// Always call hook unconditionally; only use value if filter is active
	const storeValue = store.useState("value");
	const search = filter ? storeValue : "";

	const visible = React.useMemo(
		() => !filter || filter({ group, keywords, label, value }, search),
		[filter, group, keywords, label, value, search]
	);

	if (!visible) return null;

	return (
		<ComboboxItem
			className={cn(comboboxItemVariants(), className)}
			onClick={(event) => {
				removeInput(focusEditor);
				onClick?.(event);
			}}
			{...props}
		/>
	);
};

const InlineComboboxEmpty = ({ children, className }: React.HTMLAttributes<HTMLDivElement>) => {
	const { setHasEmpty } = React.useContext(InlineComboboxContext);
	const store = useRequiredComboboxContext();
	const items = store.useState("items");

	React.useEffect(() => {
		setHasEmpty(true);

		return () => {
			setHasEmpty(false);
		};
	}, [setHasEmpty]);

	if (items.length > 0) return null;

	return (
		<div className={cn(comboboxItemVariants({ interactive: false }), className)}>{children}</div>
	);
};

const InlineComboboxRow = ComboboxRow;

function InlineComboboxGroup({ className, ...props }: React.ComponentProps<typeof ComboboxGroup>) {
	return (
		<ComboboxGroup
			{...props}
			className={cn("hidden not-last:border-b py-1.5 [&:has([role=option])]:block", className)}
		/>
	);
}

function InlineComboboxGroupLabel({
	className,
	...props
}: React.ComponentProps<typeof ComboboxGroupLabel>) {
	return (
		<ComboboxGroupLabel
			{...props}
			className={cn("mt-1.5 mb-2 px-3 font-medium text-muted-foreground text-xs", className)}
		/>
	);
}

export {
	InlineCombobox,
	InlineComboboxContent,
	InlineComboboxEmpty,
	InlineComboboxGroup,
	InlineComboboxGroupLabel,
	InlineComboboxInput,
	InlineComboboxItem,
	InlineComboboxRow,
};
