"use client";

import {
	Folder as FolderIcon,
	MessageSquare as MessageSquareIcon,
	Plug as PlugIcon,
	X as XIcon,
} from "lucide-react";
import type { NodeEntry, TElement } from "platejs";
import type { PlateElementProps } from "platejs/react";
import {
	createPlatePlugin,
	ParagraphPlugin,
	Plate,
	PlateContent,
	usePlateEditor,
} from "platejs/react";
import {
	createContext,
	type FC,
	forwardRef,
	useCallback,
	useContext,
	useImperativeHandle,
	useMemo,
	useRef,
} from "react";
import { Button } from "@/components/ui/button";
import { getConnectorIcon } from "@/contracts/enums/connectorIcons";
import type { Document } from "@/contracts/types/document.types";
import { getMentionDocKey } from "@/lib/chat/mention-doc-key";
import { cn } from "@/lib/utils";

export type MentionKind = "doc" | "folder" | "connector" | "thread";

export interface MentionedDocument {
	id: number;
	title: string;
	document_type?: string;
	kind: MentionKind;
	connector_type?: string;
	account_name?: string;
}

/**
 * Input shape for inserting a chip. ``kind`` defaults to ``"doc"``.
 */
export type MentionChipInput = {
	id: number;
	title: string;
	document_type?: string;
	kind?: MentionKind;
	connector_type?: string;
	account_name?: string;
};

export type SuggestionAnchorRect = {
	left: number;
	top: number;
	bottom: number;
};

export type SuggestionTriggerInfo = {
	query: string;
	anchorRect: SuggestionAnchorRect | null;
};

export interface InlineMentionEditorRef {
	focus: () => void;
	clear: () => void;
	setText: (text: string) => void;
	getText: () => string;
	getMentionedDocuments: () => MentionedDocument[];
	insertMentionChip: (mention: MentionChipInput, options?: { removeTriggerText?: boolean }) => void;
	/**
	 * @deprecated Use ``insertMentionChip``. Kept for one transition
	 * cycle so we don't break ad-hoc callers; prefer the new name.
	 */
	insertDocumentChip: (
		doc: Pick<Document, "id" | "title" | "document_type">,
		options?: { removeTriggerText?: boolean }
	) => void;
	removeDocumentChip: (
		docId: number,
		docType?: string,
		kind?: MentionKind,
		connectorType?: string
	) => void;
	setDocumentChipStatus: (
		docId: number,
		docType: string | undefined,
		statusLabel: string | null,
		statusKind?: "pending" | "processing" | "ready" | "failed"
	) => void;
}

interface InlineMentionEditorProps {
	placeholder?: string;
	onMentionTrigger?: (trigger: SuggestionTriggerInfo) => void;
	onMentionClose?: () => void;
	onActionTrigger?: (trigger: SuggestionTriggerInfo) => void;
	onActionClose?: () => void;
	onSubmit?: () => void;
	onChange?: (text: string, docs: MentionedDocument[]) => void;
	onDocumentRemove?: (
		docId: number,
		docType?: string,
		kind?: MentionKind,
		connectorType?: string
	) => void;
	onKeyDown?: (e: React.KeyboardEvent) => void;
	disabled?: boolean;
	className?: string;
	initialText?: string;
}

type MentionStatusKind = "pending" | "processing" | "ready" | "failed";
type ComposerTextNode = { text: string };
type MentionElementNode = {
	type: "mention";
	id: number;
	title: string;
	document_type?: string;
	/** Discriminator; defaults to ``"doc"`` for legacy nodes. */
	kind?: MentionKind;
	connector_type?: string;
	account_name?: string;
	statusLabel?: string | null;
	statusKind?: MentionStatusKind;
	children: [{ text: "" }];
};
type ComposerNode = ComposerTextNode | MentionElementNode;
type ComposerParagraph = { type: "p"; children: ComposerNode[] };
type ComposerValue = ComposerParagraph[];

const MENTION_TYPE = "mention";
const MENTION_CHIP_CLASSNAME =
	"group inline-flex h-5 items-center gap-1 mx-0.5 rounded bg-primary/10 px-1 text-xs font-bold text-primary/60 select-none align-middle leading-none";
const MENTION_CHIP_ICON_CLASSNAME = "flex items-center text-muted-foreground leading-none";
const MENTION_CHIP_TITLE_CLASSNAME = "max-w-[120px] truncate leading-none";
const COMPOSER_TEXT_METRICS_CLASSNAME = "text-sm leading-6";

const EMPTY_VALUE: ComposerValue = [{ type: "p", children: [{ text: "" }] }];

/**
 * Lets ``MentionElement`` reach the editor's chip-removal helper so
 * the X button and Backspace go through the same call site.
 */
type MentionEditorContextValue = {
	removeChip: (
		docId: number,
		docType: string | undefined,
		kind: MentionKind | undefined,
		connectorType: string | undefined
	) => void;
};
const MentionEditorContext = createContext<MentionEditorContextValue | null>(null);

const MentionElement: FC<PlateElementProps<MentionElementNode>> = ({
	attributes,
	children,
	element,
}) => {
	const statusClass =
		element.statusKind === "failed"
			? "text-destructive"
			: element.statusKind === "ready"
				? "text-emerald-700"
				: "text-amber-700";

	const isFolder = element.kind === "folder";
	const isConnector = element.kind === "connector";
	const isThread = element.kind === "thread";
	const ctx = useContext(MentionEditorContext);

	return (
		<span {...attributes} className="inline-flex align-middle">
			<span contentEditable={false} className={`${MENTION_CHIP_CLASSNAME} cursor-default`}>
				<span className={MENTION_CHIP_ICON_CLASSNAME}>
					<span className="relative flex h-3 w-3 items-center justify-center">
						<span className="flex items-center justify-center transition-opacity group-hover:opacity-0">
							{isFolder ? (
								<FolderIcon className="h-3 w-3" />
							) : isThread ? (
								<MessageSquareIcon className="h-3 w-3" />
							) : isConnector ? (
								(getConnectorIcon(
									element.connector_type ?? element.document_type ?? "UNKNOWN",
									"h-3 w-3"
								) ?? <PlugIcon className="h-3 w-3" />)
							) : (
								getConnectorIcon(element.document_type ?? "UNKNOWN", "h-3 w-3")
							)}
						</span>
						{ctx ? (
							<Button
								type="button"
								variant="ghost"
								size="icon"
								aria-label={`Remove mention ${element.title}`}
								title={`Remove ${element.title}`}
								onMouseDown={(e) => e.preventDefault()}
								onClick={(e) => {
									e.stopPropagation();
									ctx.removeChip(
										element.id,
										element.document_type,
										element.kind,
										element.connector_type
									);
								}}
								className="absolute inset-0 size-3 rounded-sm p-0 opacity-0 transition-opacity hover:bg-transparent hover:text-primary focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-0 group-hover:opacity-100 [&_svg]:size-3"
							>
								<XIcon />
							</Button>
						) : null}
					</span>
				</span>
				<span className={MENTION_CHIP_TITLE_CLASSNAME} title={element.title}>
					{element.title}
				</span>
				{element.statusLabel ? (
					<span className={cn("text-[10px] font-semibold opacity-80", statusClass)}>
						{element.statusLabel}
					</span>
				) : null}
			</span>
			{children}
		</span>
	);
};

const MentionPlugin = createPlatePlugin({
	key: MENTION_TYPE,
	node: {
		isElement: true,
		isInline: true,
		isVoid: true,
		type: MENTION_TYPE,
		component: MentionElement,
	},
});

function isMentionNode(node: ComposerNode): node is MentionElementNode {
	return typeof node === "object" && "type" in node && node.type === MENTION_TYPE;
}

function getTextNode(node: ComposerNode): ComposerTextNode | null {
	if (typeof node === "object" && "text" in node && typeof node.text === "string") return node;
	return null;
}

function toValueFromText(text: string): ComposerValue {
	const lines = text.split("\n");
	if (lines.length === 0) return EMPTY_VALUE;
	return lines.map((line) => ({ type: "p", children: [{ text: line }] })) as ComposerValue;
}

function getPlainText(value: ComposerValue): string {
	const lines = value.map((block) =>
		block.children
			.map((node) => {
				if (isMentionNode(node)) return `@${node.title}`;
				return getTextNode(node)?.text ?? "";
			})
			.join("")
	);
	return lines.join("\n").trim();
}

function getMentionedDocuments(value: ComposerValue): MentionedDocument[] {
	const map = new Map<string, MentionedDocument>();
	for (const block of value) {
		for (const node of block.children) {
			if (!isMentionNode(node)) continue;
			const kind: MentionKind = node.kind ?? "doc";
			const doc: MentionedDocument = {
				id: node.id,
				title: node.title,
				document_type: node.document_type,
				kind,
				connector_type: node.connector_type,
				account_name: node.account_name,
			};
			map.set(getMentionDocKey(doc), doc);
		}
	}
	return Array.from(map.values());
}

type EditorSelection = {
	anchor: { path: number[]; offset: number };
	focus: { path: number[]; offset: number };
} | null;

function getCursorTextContext(value: ComposerValue, selection: EditorSelection) {
	if (!selection || !selection.anchor || !selection.focus) return null;
	if (
		selection.anchor.path.length < 2 ||
		selection.focus.path.length < 2 ||
		selection.anchor.path[0] !== selection.focus.path[0] ||
		selection.anchor.path[1] !== selection.focus.path[1]
	) {
		return null;
	}

	const block = value[selection.anchor.path[0]];
	if (!block) return null;
	const child = block.children[selection.anchor.path[1]];
	const textNode = getTextNode(child);
	if (!textNode) return null;

	return {
		blockIndex: selection.anchor.path[0],
		childIndex: selection.anchor.path[1],
		text: textNode.text,
		cursor: selection.anchor.offset,
	};
}

function scanActiveTrigger(text: string, cursor: number) {
	let wordStart = 0;
	for (let i = cursor - 1; i >= 0; i--) {
		if (text[i] === " " || text[i] === "\n") {
			wordStart = i + 1;
			break;
		}
	}

	let triggerChar: "@" | "/" | null = null;
	let triggerIndex = -1;
	for (let i = wordStart; i < cursor; i++) {
		if (text[i] === "@" || text[i] === "/") {
			triggerChar = text[i] as "@" | "/";
			triggerIndex = i;
			break;
		}
	}
	if (!triggerChar || triggerIndex === -1) return null;

	const query = text.slice(triggerIndex + 1, cursor);
	if (query.startsWith(" ")) return null;
	if (
		triggerChar === "/" &&
		triggerIndex > 0 &&
		text[triggerIndex - 1] !== " " &&
		text[triggerIndex - 1] !== "\n"
	) {
		return null;
	}

	return { triggerChar, query };
}

function rectToAnchor(rect: DOMRect): SuggestionAnchorRect {
	return {
		left: rect.left,
		top: rect.top,
		bottom: rect.bottom,
	};
}

function getSelectionAnchorRect(root: HTMLElement | null): SuggestionAnchorRect | null {
	if (!root || typeof window === "undefined") return null;

	const selection = window.getSelection();
	if (!selection || selection.rangeCount === 0 || !selection.anchorNode) return null;
	if (!root.contains(selection.anchorNode)) return null;

	const range = selection.getRangeAt(0).cloneRange();
	const rect = range.getClientRects()[0] ?? range.getBoundingClientRect();
	if (rect.width > 0 || rect.height > 0) return rectToAnchor(rect);

	if (
		range.collapsed &&
		range.startContainer.nodeType === Node.TEXT_NODE &&
		range.startOffset > 0
	) {
		const fallbackRange = range.cloneRange();
		fallbackRange.setStart(range.startContainer, range.startOffset - 1);
		fallbackRange.setEnd(range.startContainer, range.startOffset);
		const fallbackRect = fallbackRange.getClientRects()[0] ?? fallbackRange.getBoundingClientRect();
		if (fallbackRect.width > 0 || fallbackRect.height > 0) return rectToAnchor(fallbackRect);
	}

	return null;
}

export const InlineMentionEditor = forwardRef<InlineMentionEditorRef, InlineMentionEditorProps>(
	(
		{
			placeholder = "Type @ to mention documents...",
			onMentionTrigger,
			onMentionClose,
			onActionTrigger,
			onActionClose,
			onSubmit,
			onChange,
			onDocumentRemove,
			onKeyDown,
			disabled = false,
			className,
			initialText,
		},
		ref
	) => {
		const editableRef = useRef<HTMLDivElement | null>(null);
		const editor = usePlateEditor({
			readOnly: disabled,
			plugins: [ParagraphPlugin, MentionPlugin],
			value: initialText ? toValueFromText(initialText) : EMPTY_VALUE,
		});

		// Move the caret to end-of-doc and focus the editor. Falls back
		// to DOM focus if Plate's API throws (transient unmount race).
		const focusAtEnd = useCallback(() => {
			try {
				editor.tf.select(editor.api.end([]));
				editor.tf.focus();
			} catch {
				editableRef.current?.focus();
			}
		}, [editor]);

		const getCurrentValue = useCallback(
			() => (editor.children as ComposerValue) ?? EMPTY_VALUE,
			[editor]
		);

		const emitState = useCallback(
			(nextValue: ComposerValue) => {
				const text = getPlainText(nextValue);
				const docs = getMentionedDocuments(nextValue);
				onChange?.(text, docs);

				const cursorCtx = getCursorTextContext(nextValue, editor.selection);
				if (!cursorCtx) {
					onMentionClose?.();
					onActionClose?.();
					return;
				}

				const trigger = scanActiveTrigger(cursorCtx.text, cursorCtx.cursor);
				if (!trigger) {
					onMentionClose?.();
					onActionClose?.();
					return;
				}

				const triggerInfo: SuggestionTriggerInfo = {
					query: trigger.query,
					anchorRect: getSelectionAnchorRect(editableRef.current),
				};

				if (trigger.triggerChar === "@") {
					onActionClose?.();
					onMentionTrigger?.(triggerInfo);
					return;
				}

				onMentionClose?.();
				onActionTrigger?.(triggerInfo);
			},
			[editor.selection, onActionClose, onActionTrigger, onChange, onMentionClose, onMentionTrigger]
		);

		const setValue = useCallback(
			(nextValue: ComposerValue) => {
				const tf = editor.tf as { setValue: (value: ComposerValue) => void };
				tf.setValue(nextValue);
				emitState(nextValue);
			},
			[editor, emitState]
		);

		// Insert chip + trailing space as a single ``insertNodes`` call.
		// The chip is a void inline; ``select: true`` on it alone would
		// land the caret inside its empty children (an unrenderable
		// point). With the space as the last inserted node, the caret
		// resolves to that text node and stays visible. The
		// ``withoutNormalizing`` wrapper batches the optional trigger
		// delete + insert into a single undo step.
		const insertMentionChip = useCallback(
			(mention: MentionChipInput, options?: { removeTriggerText?: boolean }) => {
				if (typeof mention.id !== "number" || typeof mention.title !== "string") return;

				const removeTriggerText = options?.removeTriggerText ?? true;
				const kind: MentionKind = mention.kind ?? "doc";
				const mentionNode: MentionElementNode = {
					type: MENTION_TYPE,
					id: mention.id,
					title: mention.title,
					document_type: mention.document_type,
					kind,
					connector_type: mention.connector_type,
					account_name: mention.account_name,
					children: [{ text: "" }],
				};

				editor.tf.withoutNormalizing(() => {
					const selection = editor.selection;

					// No active selection (focus moved to a picker) — snap
					// to end-of-doc so the chip appends cleanly.
					if (!selection) {
						editor.tf.select(editor.api.end([]));
					} else if (removeTriggerText) {
						// Delete the in-progress "@query" so the chip stands in for it.
						const cursorCtx = getCursorTextContext(getCurrentValue(), selection);
						if (cursorCtx) {
							const text = cursorCtx.text;
							let triggerIndex = -1;
							for (let i = cursorCtx.cursor - 1; i >= 0; i--) {
								if (text[i] === "@") {
									triggerIndex = i;
									break;
								}
								if (text[i] === " " || text[i] === "\n") break;
							}
							if (triggerIndex >= 0 && triggerIndex < cursorCtx.cursor) {
								const path = [cursorCtx.blockIndex, cursorCtx.childIndex];
								editor.tf.delete({
									at: {
										anchor: { path, offset: triggerIndex },
										focus: { path, offset: cursorCtx.cursor },
									},
								});
							}
						}
					}

					editor.tf.insertNodes([mentionNode, { text: " " }] as unknown as TElement[], {
						select: true,
					});
				});
				editor.tf.focus();
			},
			[editor, getCurrentValue]
		);

		// Doc-only shim that routes through ``insertMentionChip``.
		const insertDocumentChip = useCallback(
			(
				doc: Pick<Document, "id" | "title" | "document_type">,
				options?: { removeTriggerText?: boolean }
			) => {
				insertMentionChip({ ...doc, kind: "doc" }, options);
			},
			[insertMentionChip]
		);

		// Remove chip(s) matching the mention identity. Iterates in
		// descending path order so removing one entry can't invalidate
		// later paths. Chips are deduped today, so this typically runs
		// at most once.
		const removeDocumentChip = useCallback(
			(docId: number, docType?: string, kind?: MentionKind, connectorType?: string) => {
				const match = (n: unknown) => {
					if (!n || typeof n !== "object" || !("type" in n)) return false;
					const node = n as MentionElementNode;
					if (node.type !== MENTION_TYPE) return false;
					if (node.id !== docId) return false;
					if (kind) {
						return (
							getMentionDocKey({
								id: node.id,
								kind: node.kind ?? "doc",
								document_type: node.document_type,
								connector_type: node.connector_type,
							}) ===
							getMentionDocKey({
								id: docId,
								kind,
								document_type: docType,
								connector_type: connectorType,
							})
						);
					}
					return (node.document_type ?? "UNKNOWN") === (docType ?? "UNKNOWN");
				};

				const entries = Array.from(editor.api.nodes({ at: [], match })) as NodeEntry[];
				if (entries.length === 0) return;
				editor.tf.withoutNormalizing(() => {
					for (const [, path] of entries.reverse()) {
						editor.tf.removeNodes({ at: path });
					}
				});
			},
			[editor]
		);

		// Single removal call site for Backspace and the X button so the
		// two can never diverge (e.g. one forgetting to notify the parent).
		const removeChip = useCallback(
			(
				docId: number,
				docType: string | undefined,
				kind: MentionKind | undefined,
				connectorType: string | undefined
			) => {
				removeDocumentChip(docId, docType, kind, connectorType);
				onDocumentRemove?.(docId, docType, kind, connectorType);
			},
			[onDocumentRemove, removeDocumentChip]
		);

		// Update chip status in place via ``tf.setNodes`` so the user's
		// selection survives backend status events arriving mid-typing.
		const setDocumentChipStatus = useCallback(
			(
				docId: number,
				docType: string | undefined,
				statusLabel: string | null,
				statusKind: MentionStatusKind = "pending"
			) => {
				const match = (n: unknown) => {
					if (!n || typeof n !== "object" || !("type" in n)) return false;
					const node = n as MentionElementNode;
					if (node.type !== MENTION_TYPE) return false;
					if (node.id !== docId) return false;
					return (node.document_type ?? "UNKNOWN") === (docType ?? "UNKNOWN");
				};

				editor.tf.setNodes(
					{
						statusLabel,
						statusKind: statusLabel ? statusKind : undefined,
					} as Partial<TElement>,
					{ at: [], match }
				);
			},
			[editor]
		);

		const clear = useCallback(() => {
			setValue(EMPTY_VALUE);
			// ``tf.setValue`` wipes the selection — refocus so the caret
			// returns after Enter-to-submit.
			requestAnimationFrame(focusAtEnd);
		}, [focusAtEnd, setValue]);

		const setText = useCallback(
			(text: string) => {
				setValue(toValueFromText(text));
				requestAnimationFrame(focusAtEnd);
			},
			[focusAtEnd, setValue]
		);

		const getText = useCallback(() => getPlainText(getCurrentValue()), [getCurrentValue]);
		const getMentionedDocs = useCallback(
			() => getMentionedDocuments(getCurrentValue()),
			[getCurrentValue]
		);

		useImperativeHandle(
			ref,
			() => ({
				// Preserve existing selection if any; otherwise seed one
				// at end-of-doc so the contentEditable shows a caret.
				focus: () => {
					try {
						if (!editor.selection) {
							editor.tf.select(editor.api.end([]));
						}
						editor.tf.focus();
					} catch {
						editableRef.current?.focus();
					}
				},
				clear,
				setText,
				getText,
				getMentionedDocuments: getMentionedDocs,
				insertMentionChip,
				insertDocumentChip,
				removeDocumentChip,
				setDocumentChipStatus,
			}),
			[
				clear,
				editor,
				getMentionedDocs,
				getText,
				insertMentionChip,
				insertDocumentChip,
				removeDocumentChip,
				setDocumentChipStatus,
				setText,
			]
		);

		const handleKeyDown = useCallback(
			(e: React.KeyboardEvent<HTMLDivElement>) => {
				onKeyDown?.(e);
				if (e.defaultPrevented) return;

				if (e.key === "Enter" && !e.shiftKey) {
					e.preventDefault();
					onSubmit?.();
					return;
				}

				if (e.key !== "Backspace") return;
				const selection = editor.selection;
				if (!selection || !selection.anchor || !selection.focus) return;
				if (
					selection.anchor.path.length < 2 ||
					selection.focus.path.length < 2 ||
					selection.anchor.path[0] !== selection.focus.path[0]
				) {
					return;
				}
				if (selection.anchor.offset !== 0 || selection.focus.offset !== 0) return;

				const value = getCurrentValue();
				const block = value[selection.anchor.path[0]];
				if (!block) return;
				const childIndex = selection.anchor.path[1];
				if (childIndex <= 0) return;
				const prev = block.children[childIndex - 1];
				if (!isMentionNode(prev)) return;

				e.preventDefault();
				removeChip(prev.id, prev.document_type, prev.kind, prev.connector_type);
			},
			[editor.selection, getCurrentValue, onKeyDown, onSubmit, removeChip]
		);

		const editableProps = useMemo(
			() => ({
				placeholder,
				onPaste: (e: React.ClipboardEvent<HTMLDivElement>) => {
					e.preventDefault();
					const text = e.clipboardData.getData("text/plain");
					const tf = editor.tf as { insertText: (value: string) => void };
					tf.insertText(text);
				},
				onKeyDown: handleKeyDown,
			}),
			[editor, handleKeyDown, placeholder]
		);

		const mentionEditorContextValue = useMemo<MentionEditorContextValue>(
			() => ({ removeChip }),
			[removeChip]
		);

		return (
			<div className="relative w-full">
				<MentionEditorContext.Provider value={mentionEditorContextValue}>
					<Plate
						editor={editor}
						onChange={({ value }) => {
							emitState(value as ComposerValue);
						}}
					>
						<PlateContent
							ref={editableRef}
							readOnly={disabled}
							{...editableProps}
							className={cn(
								"min-h-[24px] max-h-32 overflow-y-auto outline-none whitespace-pre-wrap wrap-break-word",
								COMPOSER_TEXT_METRICS_CLASSNAME,
								disabled && "opacity-50 cursor-not-allowed",
								className
							)}
						/>
					</Plate>
				</MentionEditorContext.Provider>
			</div>
		);
	}
);

InlineMentionEditor.displayName = "InlineMentionEditor";
