<template>
	<el-result
		v-if="shouldShowFallback"
		class="ai-ui-card-result"
		icon="error"
		title=""
		sub-title="不支持卡片展示"
	/>
	<iframe
		v-else
		ref="iframeRef"
		class="ai-ui-card-iframe"
		:src="iframeUrl"
		:style="iframeStyle"
		frameborder="0"
		scrolling="no"
		@error="handleIframeError"
		@load="handleIframeLoad"
	/>
</template>

<script setup lang="ts">
import type { CSSProperties } from 'vue'
import { computed, onBeforeUnmount, reactive, ref, watch, onMounted } from 'vue'

interface BpmToolsWindow extends Window {
	BpmTools?: {
		$$make_ai_card_item_iframe?: (html: string) => string | undefined
		$$open_ai_card_page?: (html: string, callback: (res: string) => void) => void
	}
}

const win = window as BpmToolsWindow

const props = defineProps<{
	data: any
	attrs: Record<string, any>
}>()

const emit = defineEmits<{
	(e: 'submit', payload: string): void
}>()

const html = computed(() => {
	const text = props.data.trim()
	const { attrs } = props
	const keys = Object.keys(attrs)
	const str = `<ai-ui-card ${keys.map((k) => `${k}="${attrs[k] || ''}"`).join(' ')}>${text || ''}</ai-ui-card>`
	console.log(str)
	return str
})

const iframeUrl = computed(() => {
	return win?.BpmTools?.$$make_ai_card_item_iframe?.(html.value) || ''
})

const iframeRef = ref<HTMLIFrameElement | null>(null)
const isIframeLoadFailed = ref(false)
const iframeSize = reactive({
	width: 0,
	height: 0
})

let resizeObserver: ResizeObserver | null = null
let mutationObserver: MutationObserver | null = null
let measureRaf = 0
let validateToken = 0

const shouldShowFallback = computed(() => !iframeUrl.value || isIframeLoadFailed.value)

const iframeStyle = computed(
	(): CSSProperties => ({
		width: '100%',
		height: iframeSize.height ? `${iframeSize.height}px` : '0',
		visibility: iframeSize.height ? 'visible' : 'hidden'
	})
)

watch(
	iframeUrl,
	(url) => {
		const token = (validateToken += 1)
		isIframeLoadFailed.value = !url
		iframeSize.width = 0
		iframeSize.height = 0
		cleanupIframeWatchers()

		validateIframeUrl(url, token)
	},
	{ immediate: true }
)

onBeforeUnmount(() => {
	validateToken += 1
	cleanupIframeWatchers()
	if (measureRaf) {
		cancelAnimationFrame(measureRaf)
		measureRaf = 0
	}
})

function cleanupIframeWatchers() {
	resizeObserver?.disconnect()
	mutationObserver?.disconnect()
	resizeObserver = null
	mutationObserver = null
}

function handleIframeError() {
	isIframeLoadFailed.value = true
	cleanupIframeWatchers()
}

function handleIframeLoad() {
	if (isIframeLoadFailed.value) {
		return
	}

	cleanupIframeWatchers()

	const doc = getIframeDocument()
	if (!doc?.body) {
		return
	}

	doc.documentElement.style.overflow = 'hidden'
	doc.body.style.margin = '0'
	doc.body.style.overflow = 'hidden'

	resizeObserver = new ResizeObserver(() => {
		scheduleMeasureIframe()
	})
	resizeObserver.observe(doc.documentElement)
	resizeObserver.observe(doc.body)
	observeBodyChildren(doc.body)

	mutationObserver = new MutationObserver(() => {
		observeBodyChildren(doc.body)
		scheduleMeasureIframe()
	})
	mutationObserver.observe(doc.body, {
		attributes: true,
		characterData: true,
		childList: true,
		subtree: true
	})

	scheduleMeasureIframe()
}

async function validateIframeUrl(url: string, token: number) {
	if (!url) {
		isIframeLoadFailed.value = true
		return
	}

	const parsedUrl = getSameOriginHttpUrl(url)
	if (!parsedUrl) return

	try {
		const response = await fetch(parsedUrl, {
			method: 'HEAD',
			credentials: 'same-origin'
		})

		if (token === validateToken && !response.ok) {
			isIframeLoadFailed.value = true
			cleanupIframeWatchers()
		}
	} catch {
		if (token === validateToken) {
			isIframeLoadFailed.value = true
			cleanupIframeWatchers()
		}
	}
}

function getSameOriginHttpUrl(url: string) {
	try {
		const parsedUrl = new URL(url, window.location.href)
		if (!['http:', 'https:'].includes(parsedUrl.protocol)) return ''
		if (parsedUrl.origin !== window.location.origin) return ''
		return parsedUrl.href
	} catch {
		return ''
	}
}

function getIframeDocument() {
	const iframe = iframeRef.value
	if (!iframe) return null

	try {
		return iframe.contentDocument || iframe.contentWindow?.document || null
	} catch {
		return null
	}
}

function observeBodyChildren(body: HTMLElement) {
	if (!resizeObserver) return

	Array.from(body.children).forEach((child) => {
		resizeObserver?.observe(child)
	})
}

function scheduleMeasureIframe() {
	if (measureRaf) {
		cancelAnimationFrame(measureRaf)
	}

	measureRaf = requestAnimationFrame(() => {
		measureRaf = requestAnimationFrame(() => {
			measureRaf = 0
			measureIframe()
		})
	})
}

function measureIframe() {
	const doc = getIframeDocument()
	if (!doc?.body) return

	const size = getContentSize(doc)
	iframeSize.height = size.height
}

function getContentSize(doc: Document) {
	const body = doc.body
	const root = doc.documentElement
	const children = Array.from(body.children)

	if (!children.length) {
		return {
			width: Math.ceil(Math.max(body.scrollWidth, root.scrollWidth)),
			height: Math.ceil(Math.max(body.scrollHeight, root.scrollHeight))
		}
	}

	const bodyRect = body.getBoundingClientRect()
	let left = 0
	let top = 0
	let right = 0
	let bottom = 0

	children.forEach((child) => {
		const rect = child.getBoundingClientRect()
		left = Math.min(left, rect.left - bodyRect.left)
		top = Math.min(top, rect.top - bodyRect.top)
		right = Math.max(right, rect.right - bodyRect.left)
		bottom = Math.max(bottom, rect.bottom - bodyRect.top)
	})

	return {
		width: '100%',
		height: Math.ceil(Math.max(bottom - top, body.scrollHeight, root.scrollHeight, 1))
	}
}

/**
 * 处理iframe消息
 * @param event
 */
const handleMessage = (event: MessageEvent) => {
	const data = event.data || {}
	if (data?.type === 'ai-ui-card' && data?.data) {
		emit('submit', data.data)
	}
}

onBeforeUnmount(() => {
	window.removeEventListener('message', handleMessage)
})

onMounted(() => {
	window.addEventListener('message', handleMessage)
})
</script>

<style scoped>
.ai-ui-card-iframe {
	display: block;
	max-width: 100%;
	min-width: 360px;
	border: 0;
	overflow: hidden;
}

.ai-ui-card-result {
	width: 100%;
	min-width: 360px;
	padding: 16px 0;
}
</style>
