<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { MenuOption } from 'lexical-vue'
import { useLexicalComposer } from 'lexical-vue/LexicalComposer'
import { TypeaheadMenuPlugin } from 'lexical-vue/LexicalTypeaheadMenuPlugin'
import type { TriggerFn } from 'lexical-vue/LexicalTypeaheadMenuPlugin'
import {
	$getSelection,
	$insertNodes,
	$isRangeSelection,
	type LexicalEditor,
	type TextNode
} from 'lexical'
import { Icon } from '@repo/ui'
import { VARIABLE_TYPE_OPTIONS } from '@/constant'
import { nodeMap } from '@/nodes'
import { useI18n } from '@/composables/useI18n'
import { $createVarLabelNode } from './VarLabel'
import { getVariableTypeByValue, type VariableType } from '../utils'

type NodeMapItem = {
	icon?: string
	iconColor?: string
}

const NODE_MAP = nodeMap as Record<string, NodeMapItem>

export type PromptEditorMenuOption = {
	key: string
	label: string
	value: string
	className?: string
	type?: VariableType
	groupId?: string
	groupName?: string
	nodeType?: string
	valueType?: string
}

class VarMenuOption extends MenuOption {
	label: string
	value: string
	className?: string
	type?: VariableType
	groupId?: string
	groupName?: string
	nodeType?: string
	valueType?: string

	constructor(option: PromptEditorMenuOption, index: number) {
		super(`${option.key || option.value}-${index}`)
		this.label = option.label
		this.value = option.value
		this.className = option.className
		this.type = option.type
		this.groupId = option.groupId
		this.groupName = option.groupName
		this.nodeType = option.nodeType
		this.valueType = option.valueType
	}
}

type MenuSection = {
	key: string
	title: string
	items: Array<{
		index: number
		option: VarMenuOption
	}>
}

const props = withDefaults(
	defineProps<{
		triggerStrings?: string[]
		options: PromptEditorMenuOption[]
	}>(),
	{
		triggerStrings: () => ['/']
	}
)

const editor = useLexicalComposer()
const { t } = useI18n()
const queryKeyword = ref('')
const panelKeyword = ref('')
const isMenuOpen = ref(false)
const suppressedText = ref<string | null>(null)
const menuPanelRef = ref<HTMLElement | null>(null)

const normalizedOptions = computed(() => {
	return props.options.map((option, index) => new VarMenuOption(option, index))
})

const filteredOptions = computed(() => {
	const query = queryKeyword.value.trim().toLowerCase()
	const panel = panelKeyword.value.trim().toLowerCase()
	if (!query && !panel) return normalizedOptions.value

	return normalizedOptions.value.filter((option) => {
		const label = option.label.toLowerCase()
		const value = option.value.toLowerCase()
		const groupName = (option.groupName || '').toLowerCase()

		return [query, panel].every((keyword) => {
			if (!keyword) return true
			return label.includes(keyword) || value.includes(keyword) || groupName.includes(keyword)
		})
	})
})

const groupedSections = computed<MenuSection[]>(() => {
	const map = new Map<string, MenuSection>()

	filteredOptions.value.forEach((option, index) => {
		const key = option.groupId || 'custom'
		const title = option.groupName || t('common.nodeBase.promptEditor.customVariables')

		if (!map.has(key)) {
			map.set(key, { key, title, items: [] })
		}

		map.get(key)?.items.push({ index, option })
	})

	return [...map.values()]
})

const normalizedTriggers = computed(() => {
	return props.triggerStrings
		.map((trigger) => trigger.trim())
		.filter((trigger) => trigger.length > 0)
})

const getTextUpToAnchor = () => {
	let text: string | null = null

	editor.getEditorState().read(() => {
		const selection = $getSelection()
		if (!$isRangeSelection(selection) || !selection.isCollapsed()) return

		const anchor = selection.anchor
		if (anchor.type !== 'text') return

		const anchorNode = anchor.getNode()
		if (!anchorNode.isSimpleText()) return

		text = anchorNode.getTextContent().slice(0, anchor.offset)
	})

	return text
}

const triggerMatch: TriggerFn = (text, _lexicalEditor: LexicalEditor) => {
	if (suppressedText.value !== null) {
		if (suppressedText.value === text) {
			return null
		}
		suppressedText.value = null
	}

	let latestMatch: ReturnType<TriggerFn> = null

	for (const trigger of normalizedTriggers.value) {
		const triggerIndex = text.lastIndexOf(trigger)
		if (triggerIndex < 0) continue

		const matchingString = text.slice(triggerIndex + trigger.length)
		if (/\s/.test(matchingString)) continue

		const match = {
			leadOffset: triggerIndex,
			matchingString,
			replaceableString: `${trigger}${matchingString}`
		}

		if (!latestMatch || triggerIndex > latestMatch.leadOffset) {
			latestMatch = match
		}
	}

	return latestMatch
}

const normalizeTypeLabel = (type?: string) => {
	if (!type) return ''
	const labelMap: Record<string, string> = {
		string: t('common.nodeBase.valueTypes.string'),
		number: t('common.nodeBase.valueTypes.number'),
		boolean: t('common.nodeBase.valueTypes.boolean'),
		object: t('common.nodeBase.valueTypes.object'),
		'array[string]': t('common.nodeBase.valueTypes.arrayString'),
		'array[number]': t('common.nodeBase.valueTypes.arrayNumber'),
		'array[boolean]': t('common.nodeBase.valueTypes.arrayBoolean'),
		'array[object]': t('common.nodeBase.valueTypes.arrayObject'),
		'array[file]': t('common.nodeBase.valueTypes.arrayFile')
	}

	return labelMap[type] || VARIABLE_TYPE_OPTIONS.find((item) => item.value === type)?.label || type
}

const isImageIcon = (icon?: string) => !!icon && icon.startsWith('data:image/')

const onQueryChange = (payload: string | null) => {
	queryKeyword.value = payload || ''
}

const onOpen = () => {
	isMenuOpen.value = true
}

const onClose = () => {
	isMenuOpen.value = false
	queryKeyword.value = ''
	panelKeyword.value = ''
}

const requestCloseMenu = () => {
	suppressedText.value = getTextUpToAnchor()
	queryKeyword.value = ''
	panelKeyword.value = ''
	editor.update(() => {
		// Trigger plugin update cycle so menu closes immediately.
	})
}

const resolveOptionClassName = (option: VarMenuOption) => {
	if (option.className) return option.className
	if (option.type) return `${option.type}-label`

	const variableType = getVariableTypeByValue(option.value)
	if (variableType) return `${variableType}-label`

	return 'custom-label'
}

const onSelectOption = (payload: {
	option: VarMenuOption
	textNodeContainingQuery: TextNode | null
	closeMenu: () => void
	matchingString: string
}) => {
	const { option, textNodeContainingQuery, closeMenu } = payload

	editor.update(() => {
		const node = $createVarLabelNode(resolveOptionClassName(option), option.value)

		if (textNodeContainingQuery) {
			textNodeContainingQuery.replace(node)
		} else {
			$insertNodes([node])
		}

		closeMenu()
	})
}

const onDocumentMouseDown = (event: MouseEvent) => {
	if (!isMenuOpen.value) return

	const target = event.target as Node | null
	if (!target) return

	const clickedInsideMenu = menuPanelRef.value?.contains(target) ?? false
	if (clickedInsideMenu) return

	const rootElement = editor.getRootElement()
	requestCloseMenu()
	const clickedInsideEditor = rootElement?.contains(target) ?? false
	if (!clickedInsideEditor) {
		rootElement?.blur()
	}
}

const onEditorKeyDown = (event: KeyboardEvent) => {
	if (!isMenuOpen.value) return
	if (event.key !== 'Escape') return

	event.preventDefault()
	event.stopPropagation()
	requestCloseMenu()
}

let removeRootListener: (() => void) | null = null

onMounted(() => {
	document.addEventListener('mousedown', onDocumentMouseDown, true)

	removeRootListener = editor.registerRootListener((nextRoot, prevRoot) => {
		prevRoot?.removeEventListener('keydown', onEditorKeyDown, true)
		nextRoot?.addEventListener('keydown', onEditorKeyDown, true)
	})
})

onUnmounted(() => {
	document.removeEventListener('mousedown', onDocumentMouseDown, true)
	removeRootListener?.()
})
</script>

<template>
	<TypeaheadMenuPlugin
		:options="filteredOptions"
		:onOpen="onOpen"
		:onClose="onClose"
		:onQueryChange="onQueryChange"
		:onSelectOption="onSelectOption"
		:triggerFn="triggerMatch"
		:ignoreEntityBoundary="true"
		anchorClassName="z-[999999] translate-y-[6px]"
	>
		<template #default="{ anchorElementRef, itemProps }">
			<Teleport v-if="anchorElementRef" :to="anchorElementRef">
				<div ref="menuPanelRef" class="var-select__popover">
					<div class="var-select__panel">
						<el-input
							v-model="panelKeyword"
							clearable
							size="small"
							:placeholder="t('common.nodeBase.promptEditor.searchVariable')"
							class="var-select__search"
							@mousedown.stop
							@click.stop
							@keydown.stop
						/>

						<div class="var-select__list">
							<div v-for="section in groupedSections" :key="section.key" class="var-select__group">
								<div class="var-select__group-title">{{ section.title }}</div>
								<div
									v-for="item in section.items"
									:key="item.option.key"
									class="var-select__item"
									:class="{ 'is-active': itemProps.selectedIndex === item.index }"
									:ref="item.option.setRefElement"
									@mouseenter="itemProps.setHighlightedIndex(item.index)"
									@mousedown.prevent
									@click="itemProps.selectOptionAndCleanUp(item.option)"
								>
									<span
										class="var-select__item-prefix"
										:class="item.option.groupId"
										:style="{ background: NODE_MAP[item.option.nodeType || '']?.iconColor || '' }"
									>
										<span v-if="item.option.groupId === 'env'" class="text-6px">ENV</span>
										<span v-else-if="item.option.groupId === 'sys'" class="text-6px">SYS</span>
										<img
											v-else-if="isImageIcon(NODE_MAP[item.option.nodeType || '']?.icon)"
											:src="NODE_MAP[item.option.nodeType || '']?.icon || ''"
											alt="node icon"
											class="w-16px h-16px object-contain"
										/>
										<Icon
											v-else-if="item.option.nodeType && NODE_MAP[item.option.nodeType || '']?.icon"
											:icon="NODE_MAP[item.option.nodeType || '']?.icon || ''"
											:size="16"
										/>
										<span v-else class="text-6px">VAR</span>
									</span>
									<span class="var-select__item-name" :title="item.option.label">
										{{ item.option.label }}
									</span>
									<span class="var-select__item-type">
										{{ normalizeTypeLabel(item.option.valueType) }}
									</span>
								</div>
							</div>

							<div v-if="!groupedSections.length" class="var-select__empty">
								{{ t('common.nodeBase.promptEditor.empty') }}
							</div>
						</div>
					</div>
				</div>
			</Teleport>
		</template>
	</TypeaheadMenuPlugin>
</template>

<style scoped lang="less">
.var-select__popover {
	min-width: 320px;
	max-width: 420px;
	padding: 8px;
	border: 1px solid #dcdfe6;
	border-radius: 8px;
	background: #fff;
	box-shadow: 0 8px 24px rgb(0 0 0 / 12%);
}

.var-select__panel {
	display: flex;
	flex-direction: column;
	gap: 8px;
}

.var-select__search {
	:deep(.el-input__wrapper) {
		box-shadow: none;
		border-radius: 8px;
		background-color: #f5f5f5;
	}
}

.var-select__list {
	max-height: 260px;
	overflow-y: auto;
}

.var-select__group + .var-select__group {
	margin-top: 8px;
	border-top: 1px solid #f2f2f2;
	padding-top: 8px;
}

.var-select__group-title {
	font-size: 12px;
	color: #999;
	margin-bottom: 4px;
}

.var-select__item {
	display: flex;
	align-items: center;
	gap: 6px;
	padding: 4px 6px;
	border-radius: 6px;
	cursor: pointer;
	font-size: 12px;
	color: #333;

	&.is-active,
	&:hover {
		background-color: #f5f7ff;
	}
}

.var-select__item-prefix {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 20px;
	height: 20px;
	border-radius: 6px;
	color: #fff;
	flex-shrink: 0;

	&.env {
		background: #6366f1;
	}

	&.sys {
		background: #f97316;
	}
}

.var-select__item-name {
	flex: 1;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.var-select__item-type {
	color: #999;
	flex-shrink: 0;
}

.var-select__empty {
	padding: 12px 4px;
	font-size: 12px;
	color: #999;
	text-align: center;
}
</style>
