<template>
	<div class="file-upload-input">
		<div class="action-row" :class="{ 'action-row--single': !allowLinkInput }">
			<el-upload
				class="upload-trigger"
				action="#"
				drag
				:show-file-list="false"
				:multiple="multiple"
				:accept="acceptAttr"
				:disabled="isUploading"
				:http-request="handleUploadRequest"
				:before-upload="beforeUpload"
			>
				<el-icon class="el-icon--upload"><upload-filled /></el-icon>
				<div class="el-upload__text">拖拽到此 或 <em>点击上传</em></div>
				<template #tip>
					<div class="el-upload__tip">{{ tip }}</div>
				</template>
			</el-upload>

			<button
				v-if="allowLinkInput"
				type="button"
				class="action-button"
				:disabled="isUploading"
				@click="linkDialogVisible = true"
			>
				<Icon icon="lucide:link-2" :size="18" />
				<span>粘贴文件链接</span>
			</button>
		</div>

		<div v-if="pendingUploads.length || normalizedFiles.length" class="file-list">
			<div
				v-for="file in pendingUploads"
				:key="`pending-${file.id}`"
				class="file-item file-item--uploading"
			>
				<div class="file-item__left">
					<div class="file-badge" :class="`file-badge--${getFileVisualType(file)}`">
						<el-image
							v-if="getImagePreviewSrc(file)"
							class="file-badge__image"
							:src="getImagePreviewSrc(file)"
							:preview-src-list="[getImagePreviewSrc(file)]"
							:initial-index="0"
							fit="cover"
							preview-teleported
							hide-on-click-modal
						/>
						<span v-else>{{ getFileBadgeText(file) }}</span>
					</div>
					<div class="file-meta">
						<div class="file-name">{{ file.name }}</div>
						<div class="file-info">
							<span>{{ getFileTypeText(file) }}</span>
							<span v-if="formatFileSize(file.size)">| {{ formatFileSize(file.size) }}</span>
							<span class="file-info__status">
								{{ file.progress >= 100 ? 'Processing' : 'Uploading' }}
							</span>
						</div>
						<div class="file-progress">
							<el-progress :percentage="file.progress" :stroke-width="6" :show-text="false" />
							<span class="file-progress__text">{{ file.progress }}%</span>
						</div>
					</div>
				</div>
			</div>

			<div v-for="file in normalizedFiles" :key="file.id" class="file-item">
				<div class="file-item__left">
					<div class="file-badge" :class="`file-badge--${getFileVisualType(file)}`">
						<el-image
							v-if="getImagePreviewSrc(file)"
							class="file-badge__image"
							:src="getImagePreviewSrc(file)"
							:preview-src-list="[getImagePreviewSrc(file)]"
							:initial-index="0"
							fit="cover"
							preview-teleported
							hide-on-click-modal
						/>
						<span v-else>{{ getFileBadgeText(file) }}</span>
					</div>
					<div class="file-meta">
						<div class="file-name">{{ file.name }}</div>
						<div class="file-info">
							<span>{{ getFileTypeText(file) }}</span>
							<span v-if="formatFileSize(file.size)">· {{ formatFileSize(file.size) }}</span>
						</div>
					</div>
				</div>
				<IconButton
					link
					icon="lucide:trash-2"
					class="file-item__delete"
					@click="removeFile(file.id)"
				/>
			</div>
		</div>

		<div v-else class="empty-tip">
			{{ allowLinkInput ? '暂无文件，支持本地上传或粘贴文件链接。' : '暂无文件，支持本地上传。' }}
		</div>
	</div>

	<el-dialog
		v-if="allowLinkInput"
		v-model="linkDialogVisible"
		title="粘贴文件链接"
		width="480px"
		append-to-body
		:close-on-click-modal="false"
	>
		<el-form label-position="top">
			<el-form-item label="文件链接">
				<el-input
					v-model="linkValue"
					placeholder="请输入可访问的文件链接，例如 https://example.com/demo.pdf"
					clearable
				/>
			</el-form-item>
		</el-form>

		<template #footer>
			<div class="dialog-footer">
				<el-button @click="handleCloseLinkDialog">取消</el-button>
				<el-button type="primary" @click="handleConfirmLink">确认</el-button>
			</div>
		</template>
	</el-dialog>
</template>

<script setup lang="ts">
import { computed, onBeforeUnmount, ref, watch } from 'vue'
import { ElMessage, type UploadProgressEvent, type UploadRequestOptions } from 'element-plus'
import { UploadFilled } from '@element-plus/icons-vue'
import { Icon, IconButton } from '@repo/ui'
import { GetImage, UploadFile as uploadWorkflowFile } from '@/api'
import {
	formatFileSize,
	getAllowedExtensions,
	getFileExtension,
	getFileVisualType,
	normalizeFileExtension,
	type UploadFileType,
	type WorkflowUploadFile
} from './shared'

interface Props {
	modelValue?: WorkflowUploadFile | WorkflowUploadFile[] | null
	multiple?: boolean
	fileTypes?: UploadFileType[]
	fileExtensions?: string[]
	allowLinkInput?: boolean
	tip?: string
}

interface Emits {
	(e: 'update:modelValue', value: WorkflowUploadFile | WorkflowUploadFile[] | null): void
}

type UploadRequestError = Parameters<UploadRequestOptions['onError']>[0]
type UploadListFile = Pick<WorkflowUploadFile, 'id' | 'name' | 'extensionName' | 'size'> &
	Partial<Pick<WorkflowUploadFile, 'path'>>

interface PendingUploadFile extends UploadListFile {
	progress: number
	previewUrl?: string
}

const props = withDefaults(defineProps<Props>(), {
	modelValue: null,
	multiple: false,
	fileTypes: () => [],
	fileExtensions: () => [],
	allowLinkInput: false
})

const emit = defineEmits<Emits>()

const linkDialogVisible = ref(false)
const linkValue = ref('')
const uploadingCount = ref(0)
const localFiles = ref<WorkflowUploadFile[]>([])
const pendingUploads = ref<PendingUploadFile[]>([])
const activeUploadKeys = ref<string[]>([])
const imagePreviewUrls = ref<Record<string, string>>({})
const loadingImageKeys = new Set<string>()
const objectImageKeys = new Set<string>()

const isUploading = computed(() => uploadingCount.value > 0)

const createFileId = () =>
	`file_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`

// Persisted link values may only contain a path, so derive a readable name when possible.
const getNameFromPath = (path?: string) => {
	const value = `${path || ''}`.trim()
	if (!value) return ''

	try {
		const url = new URL(value)
		const name = url.pathname.split('/').filter(Boolean).pop() || ''
		return decodeURIComponent(name)
	} catch {
		return decodeURIComponent(value.split('/').filter(Boolean).pop() || '')
	}
}

const normalizeUploadFile = (value: Partial<WorkflowUploadFile>) => {
	const path = `${value.path || ''}`.trim()
	const name = `${value.name || ''}`.trim() || getNameFromPath(path) || '未命名文件'
	const extension = normalizeFileExtension(
		value.extensionName || getFileExtension(name) || getFileExtension(path)
	)

	return {
		id: value.id || createFileId(),
		name,
		extensionName: extension.replace(/^\./, '').toUpperCase(),
		size: Number(value.size || 0),
		path,
		contentType: value.contentType || ''
	} satisfies WorkflowUploadFile
}

const normalizeModelValue = (value: Props['modelValue']) => {
	if (Array.isArray(value)) {
		return value
			.filter((item) => item && (item.name || item.path))
			.map((item) => normalizeUploadFile(item))
	}

	if (value && typeof value === 'object' && (value.name || value.path)) {
		return [normalizeUploadFile(value)]
	}

	return []
}

watch(
	() => props.modelValue,
	(value) => {
		localFiles.value = normalizeModelValue(value)
	},
	{
		immediate: true,
		deep: true
	}
)

const normalizedFiles = computed<WorkflowUploadFile[]>({
	get: () => localFiles.value,
	set: (value) => {
		localFiles.value = value

		if (props.multiple) {
			emit('update:modelValue', value)
			return
		}

		emit('update:modelValue', value[0] || null)
	}
})

const allowedExtensions = computed(() =>
	getAllowedExtensions(props.fileTypes, props.fileExtensions)
)
const acceptAttr = computed(() =>
	allowedExtensions.value.length ? allowedExtensions.value.join(',') : undefined
)

const ensureExtensionAllowed = (extension: string) => {
	const normalizedExtension = normalizeFileExtension(extension)
	if (!allowedExtensions.value.length) {
		return true
	}

	return allowedExtensions.value.includes(normalizedExtension)
}

const getFileBadgeText = (file: Partial<WorkflowUploadFile>) => {
	const extension = file.extensionName?.trim()
	return extension ? extension.slice(0, 4).toUpperCase() : 'FILE'
}

const getFileTypeText = (file: Partial<WorkflowUploadFile>) => {
	const extension = file.extensionName?.trim()
	return extension ? extension.toUpperCase() : 'FILE'
}

const isLinkPath = (path?: string) => /^https?:\/\//i.test(`${path || ''}`.trim())

const isImageFile = (file: Partial<WorkflowUploadFile>) => getFileVisualType(file) === 'image'

const getImagePreviewSrc = (file: Partial<WorkflowUploadFile>) => {
	if (!isImageFile(file)) {
		return ''
	}

	if ('progress' in file) {
		return (file as PendingUploadFile).previewUrl || ''
	}

	return imagePreviewUrls.value[file.id || ''] || ''
}

const revokeImagePreviewUrl = (key: string) => {
	const url = imagePreviewUrls.value[key]
	if (url && objectImageKeys.has(key)) {
		URL.revokeObjectURL(url)
		objectImageKeys.delete(key)
	}

	const { [key]: _removed, ...nextUrls } = imagePreviewUrls.value
	imagePreviewUrls.value = nextUrls
	loadingImageKeys.delete(key)
}

const setImagePreviewUrl = (key: string, url: string, isObjectUrl = false) => {
	const currentUrl = imagePreviewUrls.value[key]
	if (currentUrl && currentUrl !== url && objectImageKeys.has(key)) {
		URL.revokeObjectURL(currentUrl)
		objectImageKeys.delete(key)
	}

	imagePreviewUrls.value = {
		...imagePreviewUrls.value,
		[key]: url
	}

	if (isObjectUrl) {
		objectImageKeys.add(key)
	}
}

const ensureImagePreviewUrl = async (file: WorkflowUploadFile) => {
	if (!isImageFile(file) || !file.id || !file.path || imagePreviewUrls.value[file.id]) {
		return
	}

	if (isLinkPath(file.path)) {
		setImagePreviewUrl(file.id, file.path)
		return
	}

	if (loadingImageKeys.has(file.id)) {
		return
	}

	loadingImageKeys.add(file.id)
	try {
		const blob = (await GetImage({ fileId: file.path })) as unknown as Blob
		if (!blob || !normalizedFiles.value.some((item) => item.id === file.id)) {
			return
		}

		setImagePreviewUrl(file.id, URL.createObjectURL(blob), true)
	} catch (error) {
		console.error('get image preview error', error)
	} finally {
		loadingImageKeys.delete(file.id)
	}
}

watch(
	normalizedFiles,
	(files) => {
		const fileIds = new Set(files.map((file) => file.id))
		Object.keys(imagePreviewUrls.value).forEach((key) => {
			if (!fileIds.has(key)) {
				revokeImagePreviewUrl(key)
			}
		})

		files.forEach((file) => {
			void ensureImagePreviewUrl(file)
		})
	},
	{
		immediate: true,
		deep: true
	}
)

const getWorkflowFileDuplicateKey = (file: Partial<WorkflowUploadFile>) => {
	const path = `${file.path || ''}`.trim()
	const source = isLinkPath(path) ? 'link' : 'local'

	if (source === 'link') {
		return `link::${path}`
	}

	const name = `${file.name || getNameFromPath(path) || ''}`.trim().toLowerCase()
	const extension = normalizeFileExtension(
		file.extensionName || getFileExtension(file.name || path)
	)
	const size = Number(file.size || 0)

	return `local::${name}::${size}::${extension}`
}

const getRawFileDuplicateKey = (file: Pick<File, 'name' | 'size'>) => {
	const name = `${file.name || ''}`.trim().toLowerCase()
	const extension = normalizeFileExtension(getFileExtension(file.name))
	const size = Number(file.size || 0)

	return `local::${name}::${size}::${extension}`
}

const hasDuplicateFile = (duplicateKey: string) =>
	normalizedFiles.value.some((item) => getWorkflowFileDuplicateKey(item) === duplicateKey) ||
	activeUploadKeys.value.includes(duplicateKey)

const createPendingUpload = (file: File): PendingUploadFile => {
	const extension = getFileExtension(file.name)
	const pendingFile = {
		id: createFileId(),
		name: file.name,
		extensionName: extension.replace(/^\./, '').toUpperCase(),
		size: file.size,
		progress: 0
	}

	if (!isImageFile(pendingFile)) {
		return pendingFile
	}

	return {
		...pendingFile,
		previewUrl: URL.createObjectURL(file)
	}
}

const normalizeProgress = (value: number) => {
	if (!Number.isFinite(value)) {
		return 0
	}

	const percent = value > 0 && value < 1 ? value * 100 : value
	return Math.min(100, Math.max(0, Math.round(percent)))
}

const updatePendingUploadProgress = (id: string, progress: number) => {
	const target = pendingUploads.value.find((item) => item.id === id)
	if (!target) {
		return
	}

	target.progress = normalizeProgress(progress)
}

const removePendingUpload = (id: string) => {
	const target = pendingUploads.value.find((item) => item.id === id)
	if (target?.previewUrl) {
		URL.revokeObjectURL(target.previewUrl)
	}

	pendingUploads.value = pendingUploads.value.filter((item) => item.id !== id)
}

// The upload API currently returns a file id, so keep it in `path` as the persisted reference.
const buildFileFromUploadedFile = (file: File, fileId: string): WorkflowUploadFile => {
	const extension = getFileExtension(file.name)

	return {
		id: fileId || createFileId(),
		name: file.name,
		extensionName: extension.replace(/^\./, '').toUpperCase(),
		size: file.size,
		path: fileId,
		contentType: file.type
	}
}

const buildFileFromLink = (value: string): WorkflowUploadFile => {
	const url = new URL(value)
	const name = decodeURIComponent(url.pathname.split('/').filter(Boolean).pop() || 'linked-file')
	const extension = getFileExtension(name || value)

	return {
		id: createFileId(),
		name,
		extensionName: extension.replace(/^\./, '').toUpperCase(),
		size: 0,
		path: value,
		contentType: ''
	}
}

const updateFiles = (files: WorkflowUploadFile[]) => {
	normalizedFiles.value = props.multiple ? files : files.slice(0, 1)
}

const appendFiles = (files: WorkflowUploadFile[]) => {
	updateFiles(props.multiple ? [...normalizedFiles.value, ...files] : files.slice(0, 1))
}

const beforeUpload = (rawFile: File) => {
	if (!ensureExtensionAllowed(getFileExtension(rawFile.name))) {
		ElMessage.error('文件类型不符合当前字段配置')
		return false
	}

	if (props.multiple) {
		const duplicateKey = getRawFileDuplicateKey(rawFile)
		if (hasDuplicateFile(duplicateKey)) {
			ElMessage.warning('文件已存在，请勿重复上传')
			return false
		}

		activeUploadKeys.value = [...activeUploadKeys.value, duplicateKey]
	}

	return true
}

const handleUploadRequest = async (options: UploadRequestOptions) => {
	uploadingCount.value += 1
	const duplicateKey = getRawFileDuplicateKey(options.file)
	const pendingUpload = createPendingUpload(options.file)
	pendingUploads.value = [...pendingUploads.value, pendingUpload]

	try {
		const formData = new FormData()
		formData.append('files', options.file)
		const res = await uploadWorkflowFile(formData, (progress) => {
			const percent = normalizeProgress(progress)
			updatePendingUploadProgress(pendingUpload.id, percent)
			options.onProgress({ percent } as UploadProgressEvent)
		})
		const fileId = res?.id
		if (!fileId) {
			throw new Error('upload file id is missing')
		}

		updatePendingUploadProgress(pendingUpload.id, 100)
		appendFiles([buildFileFromUploadedFile(options.file, fileId)])
		removePendingUpload(pendingUpload.id)
		options.onSuccess(res)
	} catch (error) {
		console.error('upload workflow file error', error)
		ElMessage.error('文件上传失败')
		options.onError(error as UploadRequestError)
	} finally {
		removePendingUpload(pendingUpload.id)
		activeUploadKeys.value = activeUploadKeys.value.filter((item) => item !== duplicateKey)
		uploadingCount.value = Math.max(0, uploadingCount.value - 1)
	}
}

const handleCloseLinkDialog = () => {
	linkDialogVisible.value = false
	linkValue.value = ''
}

const handleConfirmLink = () => {
	const value = linkValue.value.trim()
	if (!value) {
		ElMessage.warning('请输入文件链接')
		return
	}

	try {
		const file = buildFileFromLink(value)
		if (props.multiple && hasDuplicateFile(getWorkflowFileDuplicateKey(file))) {
			ElMessage.warning('链接文件已存在，请勿重复添加')
			return
		}

		const extension = normalizeFileExtension(file.extensionName)

		if (allowedExtensions.value.length && (!extension || !ensureExtensionAllowed(extension))) {
			ElMessage.error('链接文件类型不符合当前字段配置')
			return
		}

		appendFiles([file])
		handleCloseLinkDialog()
	} catch (error) {
		console.error('invalid file url', error)
		ElMessage.error('请输入合法的文件链接')
	}
}

const removeFile = (id: string) => {
	revokeImagePreviewUrl(id)
	updateFiles(normalizedFiles.value.filter((item) => item.id !== id))
}

onBeforeUnmount(() => {
	pendingUploads.value.forEach((file) => {
		if (file.previewUrl) {
			URL.revokeObjectURL(file.previewUrl)
		}
	})

	Object.keys(imagePreviewUrls.value).forEach(revokeImagePreviewUrl)
})
</script>

<style scoped lang="less">
.file-upload-input {
	display: flex;
	flex-direction: column;
	gap: 12px;
	width: 100%;
}

.action-row {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: 10px;
}

.action-row--single {
	grid-template-columns: minmax(0, 1fr);
}

.upload-trigger {
	display: block;
}

.upload-trigger :deep(.el-upload) {
	display: block;
	width: 100%;
}

.action-button {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: 8px;
	width: 100%;
	height: 48px;
	border-radius: 12px;
	border: 1px solid #eaecf0;
	background: #f8fafc;
	color: #344054;
	font-size: 14px;
	font-weight: 500;
	cursor: pointer;
}

.action-button:hover:not(:disabled) {
	background: #eef4ff;
	border-color: #b2ccff;
	color: #296dff;
}

.action-button:disabled {
	cursor: not-allowed;
	opacity: 0.7;
}

.file-list {
	display: flex;
	flex-direction: column;
	gap: 10px;
}

.file-item {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: 12px;
	padding: 12px 14px;
	border: 1px solid #eaecf0;
	border-radius: 14px;
	background: #fff;
	box-shadow: 0 4px 12px rgba(16, 24, 40, 0.05);
}

.file-item--uploading {
	border-color: #b2ccff;
	background: #f8fbff;
}

.file-item--uploading .file-item__left {
	align-items: flex-start;
}

.file-item__left {
	display: flex;
	align-items: center;
	gap: 12px;
	min-width: 0;
	flex: 1;
}

.file-badge {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 42px;
	height: 42px;
	border-radius: 10px;
	overflow: hidden;
	font-size: 12px;
	font-weight: 700;
	flex-shrink: 0;
}

.file-badge__image {
	width: 100%;
	height: 100%;
	cursor: zoom-in;
}

.file-badge__image :deep(.el-image__inner) {
	width: 100%;
	height: 100%;
}

:global(.el-image-viewer__img) {
	max-width: min(92vw, 960px);
	max-height: min(88vh, 720px);
	object-fit: contain;
}

.file-badge--document {
	background: #eaf7ee;
	color: #0f9d58;
}

.file-badge--image {
	background: #eef4ff;
	color: #296dff;
}

.file-badge--audio {
	background: #fff1f3;
	color: #dd2590;
}

.file-badge--video {
	background: #eef2ff;
	color: #4f46e5;
}

.file-badge--custom {
	background: #f4f4f5;
	color: #52525b;
}

.file-meta {
	min-width: 0;
	flex: 1;
}

.file-name {
	font-size: 14px;
	font-weight: 500;
	color: #344054;
	word-break: break-all;
}

.file-info {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: 8px;
	margin-top: 4px;
	font-size: 12px;
	color: #667085;
}

.file-info__status {
	color: #296dff;
	font-weight: 600;
}

.file-progress {
	display: flex;
	align-items: center;
	gap: 12px;
	margin-top: 10px;
}

.file-progress :deep(.el-progress) {
	flex: 1;
}

.file-progress :deep(.el-progress-bar__outer) {
	background: #dbe7ff;
}

.file-progress__text {
	min-width: 38px;
	font-size: 12px;
	font-weight: 600;
	color: #296dff;
	text-align: right;
}

.file-item__delete {
	color: #98a2b3;
}

.empty-tip {
	padding: 14px 16px;
	border: 1px dashed #d0d5dd;
	border-radius: 12px;
	font-size: 12px;
	color: #667085;
	background: #f8fafc;
}

.dialog-footer {
	display: flex;
	justify-content: flex-end;
	gap: 8px;
}
</style>
