<template>
	<div class="video-editor-root">
		<div v-if="showToolbar" class="toolbar">
			<button class="tool-btn" type="button" @click="openImagePicker">插入参考素材</button>
			<button class="tool-btn" type="button" @click="clear">清空所有参考素材和文字</button>
		</div>
		<input
			ref="fileInputRef"
			class="hidden-input"
			type="file"
			accept="image/*,video/*,audio/*"
			multiple
			@change="handleSelectFiles" />
		<div class="editor-wrapper">
			<div
				ref="editorRef"
				class="user-input video-rich-editor"
				contenteditable="true"
				:data-placeholder="placeholder || '请输入文本生成视频...'"
				@input="handleInput"
				@paste="handlePaste"
				@keyup="handleKeyup"
				@click="handleEditorClick"></div>
			<div v-if="mentionVisible" class="mention-panel">
				<div
					v-for="item in mentionImageList"
					:key="item.url"
					class="mention-item"
					@mousedown.prevent="insertMentionImage(item)">
					<img v-if="item.mediaType === 'image'" :src="item.url" class="mention-thumb" alt="" />
					<span v-else class="mention-type-badge">{{ item.mediaType === 'video' ? '视频' : '音频' }}</span>
					<span class="mention-label">参考{{ item.refNo }}</span>
				</div>
				<div v-if="mentionImageList.length === 0" class="mention-empty">暂无可引用素材</div>
			</div>
		</div>
	</div>
</template>

<script setup>
import { nextTick, onMounted, ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import { uploadFile, extractUploadUrlFromResponse, PORTAL_TENCENT_COS_UPLOAD_URL } from '@/utils/file'

const props = defineProps({
	placeholder: {
		type: String,
		default: ''
	},
	/** 文生视频等场景可隐藏工具栏，仅保留纯文本编辑 */
	showToolbar: {
		type: Boolean,
		default: true
	}
})

const emit = defineEmits(['text-change'])

const editorRef = ref(null)
const fileInputRef = ref(null)
const savedSelectionRange = ref(null)
const MAX_IMAGE_COUNT = 9
const mentionVisible = ref(false)
const mentionImageList = ref([])

const detectMediaType = (file) => {
	const mime = (file?.type || '').toLowerCase()
	if (mime.startsWith('image/')) return 'image'
	if (mime.startsWith('video/')) return 'video'
	if (mime.startsWith('audio/')) return 'audio'
	const lowerName = (file?.name || '').toLowerCase()
	if (/\.(png|jpe?g|gif|webp|bmp|svg)$/.test(lowerName)) return 'image'
	if (/\.(mp4|mov|webm|m4v|avi|mkv)$/.test(lowerName)) return 'video'
	if (/\.(mp3|wav|aac|m4a|ogg|flac)$/.test(lowerName)) return 'audio'
	return 'image'
}

const getPlainText = () => (editorRef.value?.innerText || '').trim()

const focusEditorToEnd = () => {
	if (!editorRef.value) return
	editorRef.value.focus()
	const selection = window.getSelection()
	if (!selection) return
	const range = document.createRange()
	range.selectNodeContents(editorRef.value)
	range.collapse(false)
	selection.removeAllRanges()
	selection.addRange(range)
	savedSelectionRange.value = range.cloneRange()
}

const saveSelection = () => {
	const selection = window.getSelection()
	if (!selection || selection.rangeCount === 0) return
	const range = selection.getRangeAt(0)
	if (editorRef.value?.contains(range.commonAncestorContainer)) {
		savedSelectionRange.value = range.cloneRange()
	}
}

const restoreSelection = () => {
	const selection = window.getSelection()
	if (!selection || !editorRef.value) return
	selection.removeAllRanges()
	if (savedSelectionRange.value) {
		selection.addRange(savedSelectionRange.value)
	} else {
		const range = document.createRange()
		range.selectNodeContents(editorRef.value)
		range.collapse(false)
		selection.addRange(range)
	}
}

const handleInput = () => {
	emit('text-change', getPlainText())
	saveSelection()
	syncMentionPanelVisibility()
}

const insertPlainTextAtCursor = (text) => {
	if (!editorRef.value) return
	const normalizedText = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
	editorRef.value.focus()
	restoreSelection()
	const selection = window.getSelection()
	if (!selection || selection.rangeCount === 0) return
	let range = selection.getRangeAt(0)
	if (!editorRef.value.contains(range.commonAncestorContainer)) {
		focusEditorToEnd()
		const currentSelection = window.getSelection()
		if (!currentSelection || currentSelection.rangeCount === 0) return
		range = currentSelection.getRangeAt(0)
	}
	range.deleteContents()
	const lines = normalizedText.split('\n')
	const fragment = document.createDocumentFragment()
	lines.forEach((line, index) => {
		if (line) fragment.appendChild(document.createTextNode(line))
		if (index < lines.length - 1) fragment.appendChild(document.createElement('br'))
	})
	range.insertNode(fragment)
	range.collapse(false)
	selection.removeAllRanges()
	selection.addRange(range)
	saveSelection()
}

const handlePaste = (event) => {
	if (!editorRef.value) return
	event.preventDefault()
	const clipboardText = event.clipboardData?.getData('text/plain') || ''
	if (!clipboardText) return
	insertPlainTextAtCursor(clipboardText)
	emit('text-change', getPlainText())
	syncMentionPanelVisibility()
}

const updateMentionImageList = () => {
	mentionImageList.value = mentionImageList.value.slice(0, MAX_IMAGE_COUNT)
}

const hideMentionPanel = () => {
	mentionVisible.value = false
}

const hasActiveMentionTriggerBeforeCursor = () => {
	const selection = window.getSelection()
	if (!selection || selection.rangeCount === 0) return false
	const range = selection.getRangeAt(0)
	if (!range.collapsed) return false
	if (!editorRef.value?.contains(range.commonAncestorContainer)) return false
	const container = range.startContainer
	if (container.nodeType !== Node.TEXT_NODE) return false
	const before = container.data.slice(0, range.startOffset)
	const atIndex = before.lastIndexOf('@')
	if (atIndex < 0) return false
	const afterAt = before.slice(atIndex + 1)
	return !/\s/.test(afterAt)
}

const syncMentionPanelVisibility = () => {
	const shouldShow = hasActiveMentionTriggerBeforeCursor()
	if (!shouldShow) {
		hideMentionPanel()
		return
	}
	updateMentionImageList()
	mentionVisible.value = true
}

const removeMentionKeywordBeforeCursor = () => {
	const selection = window.getSelection()
	if (!selection || selection.rangeCount === 0) return
	const range = selection.getRangeAt(0)
	if (!range.collapsed) return
	const container = range.startContainer
	if (container.nodeType !== Node.TEXT_NODE) return
	const before = container.data.slice(0, range.startOffset)
	const atIndex = before.lastIndexOf('@')
	if (atIndex < 0) return
	const afterAt = before.slice(atIndex + 1)
	if (/\s/.test(afterAt)) return
	const removeRange = document.createRange()
	removeRange.setStart(container, atIndex)
	removeRange.setEnd(container, range.startOffset)
	removeRange.deleteContents()
}

const insertReference = (item) => {
	if (!item?.url || !editorRef.value) return
	editorRef.value.focus()
	restoreSelection()
	const selection = window.getSelection()
	if (!selection || selection.rangeCount === 0) return
	const range = selection.getRangeAt(0)
	if (!editorRef.value.contains(range.commonAncestorContainer)) return

	const refNoText = `[图${item.refNo}]`
	let node
	if (item.mediaType === 'image') {
		const img = document.createElement('img')
		img.src = item.url
		img.alt = `reference-${item.refNo}`
		img.className = 'inline-rich-image'
		img.setAttribute('data-image-url', item.url)
		img.setAttribute('data-mention-reference', '1')
		img.setAttribute('data-reference-no', String(item.refNo))
		node = img
	} else {
		const tag = document.createElement('span')
		tag.className = 'inline-rich-reference'
		tag.textContent = refNoText
		tag.setAttribute('data-reference-url', item.url)
		tag.setAttribute('data-mention-reference', '1')
		tag.setAttribute('data-reference-no', String(item.refNo))
		tag.setAttribute('data-reference-type', item.mediaType)
		node = tag
	}

	range.insertNode(node)
	const space = document.createTextNode(' ')
	range.setStartAfter(node)
	range.insertNode(space)
	range.setStartAfter(space)
	range.collapse(true)
	selection.removeAllRanges()
	selection.addRange(range)
	saveSelection()
}

const insertMentionImage = (item) => {
	if (!item?.url || !editorRef.value) return
	editorRef.value.focus()
	restoreSelection()
	removeMentionKeywordBeforeCursor()
	// 删除 @关键字 后必须重新记录选区，否则 insertReference 里 restoreSelection 仍用删除前的 Range，会插错位置甚至覆盖后文
	saveSelection()
	insertReference(item)
	hideMentionPanel()
	emit('text-change', getPlainText())
}

const handleKeyup = (event) => {
	saveSelection()
	if (event.key === '@') {
		syncMentionPanelVisibility()
		return
	}
	if (event.key === 'Escape') {
		hideMentionPanel()
		return
	}
	syncMentionPanelVisibility()
}

const handleEditorClick = () => {
	const selection = window.getSelection()
	if (!selection || selection.rangeCount === 0) {
		focusEditorToEnd()
		return
	}
	const range = selection.getRangeAt(0)
	if (!editorRef.value?.contains(range.commonAncestorContainer)) {
		focusEditorToEnd()
		return
	}
	saveSelection()
	syncMentionPanelVisibility()
}

const getInsertedUniqueImageCount = () => mentionImageList.value.length

const openImagePicker = () => {
	const current = getInsertedUniqueImageCount()
	if (current >= MAX_IMAGE_COUNT) {
		Message.warning(`最多插入 ${MAX_IMAGE_COUNT} 个参考素材`)
		return
	}
	fileInputRef.value?.click()
}

const uploadToCos = async (file) => {
	const res = await uploadFile({
		url: PORTAL_TENCENT_COS_UPLOAD_URL,
		file,
		name: 'file'
	})
	const codeOk = res && (Number(res.code) === 200 || res.code === 200)
	const url = extractUploadUrlFromResponse(res)
	if (codeOk && url) return url
	throw new Error(res?.msg || '上传失败：未返回文件地址')
}

const handleSelectFiles = async (event) => {
	const input = event.target
	const files = Array.from(input.files || [])
	if (!files.length) return
	const remain = MAX_IMAGE_COUNT - getInsertedUniqueImageCount()
	if (remain <= 0) {
		Message.warning(`最多插入 ${MAX_IMAGE_COUNT} 个参考素材`)
		input.value = ''
		return
	}
	const selected = files.slice(0, remain)
	if (files.length > remain) {
		Message.warning(`最多插入 ${MAX_IMAGE_COUNT} 个参考素材，本次仅插入 ${remain} 个`)
	}
	for (const file of selected) {
		const mediaType = detectMediaType(file)
		try {
			const url = await uploadToCos(file)
			if (url && !mentionImageList.value.some((item) => item.url === url)) {
				const refNo = mentionImageList.value.length + 1
				mentionImageList.value.push({ url, refNo, mediaType })
			}
		} catch (e) {
			Message.error('参考素材上传失败')
			console.error(e)
		}
	}
	input.value = ''
	emit('text-change', getPlainText())
}

const getContentItems = () => {
	const editor = editorRef.value
	const items = []
	if (!editor) return items
	let textBuffer = ''
	const flushText = () => {
		const normalized = textBuffer.replace(/\u00a0/g, ' ').replace(/\s+\n/g, '\n')
		if (normalized.trim()) items.push({ type: 'text', text: normalized.trim() })
		textBuffer = ''
	}
	const walk = (node) => {
		if (node.nodeType === Node.TEXT_NODE) {
			textBuffer += node.textContent || ''
			return
		}
		if (node.nodeType !== Node.ELEMENT_NODE) return
		const el = node
		const referenceUrl = (
			el.dataset?.referenceUrl ||
			el.dataset?.imageUrl ||
			el.getAttribute('src') ||
			''
		).trim()
		if (el.dataset?.mentionReference === '1' && referenceUrl) {
			flushText()
			items.push({
				type: 'image_url',
				image_url: { url: referenceUrl },
				role: 'reference_image'
			})
			return
		}
		if (el.tagName === 'IMG' && (el.dataset?.imageUrl || el.getAttribute('src'))) {
			return
		}
		if (el.tagName === 'BR') {
			textBuffer += '\n'
			return
		}
		if (['DIV', 'P', 'LI'].includes(el.tagName) && textBuffer && !textBuffer.endsWith('\n')) textBuffer += '\n'
		Array.from(el.childNodes).forEach(walk)
		if (['DIV', 'P', 'LI'].includes(el.tagName) && textBuffer && !textBuffer.endsWith('\n')) textBuffer += '\n'
	}
	Array.from(editor.childNodes).forEach(walk)
	flushText()
	return items
}

const clear = () => {
	if (!editorRef.value) return
	editorRef.value.innerHTML = ''
	emit('text-change', '')
	savedSelectionRange.value = null
	mentionImageList.value = []
	hideMentionPanel()
}

/** 供父组件持久化：参考图模式的 HTML + 素材列表 */
const getDraftState = () => ({
	html: editorRef.value ? editorRef.value.innerHTML : '',
	mentionList: mentionImageList.value.map(({ url, refNo, mediaType }) => ({
		url,
		refNo,
		mediaType
	}))
})

const applyDraftState = (draft) => {
	hideMentionPanel()
	savedSelectionRange.value = null
	if (!draft || typeof draft !== 'object') {
		clear()
		return
	}
	mentionImageList.value = Array.isArray(draft.mentionList)
		? draft.mentionList.map((x) => ({
				url: x.url,
				refNo: Number(x.refNo) || 0,
				mediaType: x.mediaType || 'image'
		  }))
		: []
	if (editorRef.value) {
		editorRef.value.innerHTML = typeof draft.html === 'string' ? draft.html : ''
	}
	nextTick(() => {
		emit('text-change', getPlainText())
	})
}

defineExpose({
	getContentItems,
	getPlainText,
	clear,
	getDraftState,
	applyDraftState
})

onMounted(() => {
	nextTick(() => {
		focusEditorToEnd()
	})
})
</script>

<style scoped>
.video-editor-root {
	display: flex;
	flex-direction: column;
	gap: 8px;
	flex: 1;
	min-height: 0;
}
.toolbar {
	display: flex;
	gap: 8px;
	flex-wrap: wrap;
}
.hidden-input {
	display: none;
}
.editor-wrapper {
	position: relative;
	flex: 1;
	display: flex;
	flex-direction: column;
	min-height: 200px;
}
.tool-btn {
	border: 1px solid rgba(255, 255, 255, 0.12);
	border-radius: 8px;
	background: rgba(255, 255, 255, 0.06);
	color: rgba(255, 255, 255, 0.88);
	padding: 6px 12px;
	cursor: pointer;
	font-size: 12px;
	transition: border-color 0.2s, background 0.2s;
}
.tool-btn:hover {
	border-color: rgba(0, 202, 224, 0.4);
	background: rgba(0, 202, 224, 0.08);
}
.video-rich-editor {
	width: 100%;
	flex: 1;
	min-height: 220px;
	font-size: 15px;
	line-height: 1.9;
	white-space: pre-wrap;
	word-break: break-word;
	overflow-y: auto;
	color: rgba(255, 255, 255, 0.9);
	caret-color: #00cae0;

	:deep(*) {
		color: rgba(255, 255, 255, 0.9) !important;
	}

	:deep(.inline-rich-reference) {
		color: #5eebf5 !important;
		background: rgba(0, 202, 224, 0.15);
	}
	border: 1px solid rgba(255, 255, 255, 0.1);
	border-radius: 14px;
	padding: 16px 18px;
	background: rgba(0, 0, 0, 0.25);
	box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
.video-rich-editor:focus {
	border-color: rgba(0, 202, 224, 0.45);
	box-shadow: 0 0 0 2px rgba(0, 202, 224, 0.15);
	outline: none;
}
.video-rich-editor:empty::before {
	content: attr(data-placeholder);
	color: rgba(255, 255, 255, 0.35);
	pointer-events: none;
	display: block;
}
.video-rich-editor :deep(.inline-rich-image) {
	display: inline-block;
	width: auto;
	height: auto;
	max-width: min(260px, 100%);
	max-height: 148px;
	object-fit: contain;
	vertical-align: middle;
	margin: 6px 8px;
	border-radius: 8px;
	border: 1px solid rgba(255, 255, 255, 0.12);
	box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
}
.video-rich-editor :deep(.inline-rich-reference) {
	display: inline-flex;
	align-items: center;
	padding: 0 6px;
	margin: 0 3px;
	border-radius: 4px;
	background: rgba(0, 202, 224, 0.15);
	color: #5eebf5;
	font-size: 0.85em;
	line-height: 1.5;
}
.mention-panel {
	position: absolute;
	left: 8px;
	right: 8px;
	bottom: 8px;
	max-height: 180px;
	overflow-y: auto;
	border: 1px solid rgba(255, 255, 255, 0.1);
	border-radius: 10px;
	background: rgba(22, 24, 30, 0.98);
	box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
	z-index: 20;
}
.mention-item {
	display: flex;
	align-items: center;
	gap: 8px;
	padding: 8px 10px;
	cursor: pointer;
}
.mention-item:hover {
	background: rgba(255, 255, 255, 0.06);
}
.mention-thumb {
	width: 32px;
	height: 32px;
	border-radius: 4px;
	object-fit: cover;
	border: 1px solid rgba(255, 255, 255, 0.1);
}
.mention-type-badge {
	min-width: 32px;
	height: 32px;
	border-radius: 4px;
	border: 1px solid rgba(255, 255, 255, 0.1);
	display: inline-flex;
	align-items: center;
	justify-content: center;
	font-size: 11px;
	color: rgba(255, 255, 255, 0.65);
	background: rgba(0, 0, 0, 0.25);
}
.mention-label {
	font-size: 12px;
	color: rgba(255, 255, 255, 0.85);
}
.mention-empty {
	padding: 8px 10px;
	font-size: 12px;
	color: rgba(255, 255, 255, 0.4);
}
</style>
