<template>
	<div
		v-if="hasVisibleBody"
		class="message-item"
		:class="[message.role === 'user' ? 'is-right' : 'is-left']"
	>
		<div class="message-item__inner">
			<div class="message-item__desc" v-if="message.skillName">
				<div class="tool">
					{{ message.skillName }}
					<el-icon class="is-loading" v-if="!message.content">
						<Loading />
					</el-icon>
				</div>
			</div>

			<thinking-block
				v-if="message.thinking && message.role === 'assistant'"
				:content="message.thinking"
				:is-streaming="false"
			/>

			<slot name="before-content" />

			<div class="message-item__content" v-if="message.content" @click="onContentClick">
				<span v-if="message.role === 'user'">{{ displayContent }}</span>
				<cl-preview-md :content="renderedContent" v-else />
			</div>

			<message-attachments
				v-if="message.role === 'user' && message.metadata?.attachments?.length"
				:attachments="message.metadata.attachments"
			/>

			<div
				v-if="toolExecutions.length && message.role === 'assistant'"
				class="message-item__tool-executions"
			>
				<template v-for="tool in toolExecutions" :key="tool.key">
					<todo-card
						v-if="tool.toolName === 'todo' && parseTodoResult(tool.result)"
						:items="parseTodoResult(tool.result)!.items"
						:summary="parseTodoResult(tool.result)!.summary"
					/>
					<div
						v-else
						class="tool-execution"
						:class="`is-${tool.status}`"
					>
					<div class="tool-execution__head">
						<span class="tool-execution__name">{{ tool.label }}</span>
						<span class="tool-execution__status">{{ tool.statusLabel }}</span>
					</div>
					<div v-if="tool.metaItems.length" class="tool-execution__meta">
						<span
							v-for="item in tool.metaItems"
							:key="`${item.label}:${item.value}`"
							class="tool-execution__meta-pill"
							:class="item.kind ? `is-${item.kind}` : undefined"
							:title="item.value"
						>
							<span class="tool-execution__meta-label">{{ item.label }}</span>
							<span class="tool-execution__meta-value">{{ item.value }}</span>
						</span>
					</div>
					<pre
						v-if="tool.showArgs && tool.args"
						class="tool-execution__args"
					>{{ stringifyContent(tool.args) }}</pre>
					<pre
						v-if="tool.result"
						class="tool-execution__result"
						:class="{ 'is-collapsed': !isToolExpanded(tool.key) && tool.isLongResult }"
					>{{ tool.result }}</pre>
					<div
						v-if="tool.rawResult?.type === 'image' && tool.rawResult.url"
						class="tool-execution__image"
					>
						<el-image
							:src="resolveBackendAssetUrl(tool.rawResult.url)"
							fit="contain"
							:preview-src-list="[resolveBackendAssetUrl(tool.rawResult.url)]"
							preview-teleported
							class="tool-execution__image-single"
						/>
						<div class="tool-execution__image-caption">
							{{ formatToolImageCaption(tool.rawResult) }}
						</div>
					</div>
					<div
						v-else-if="tool.rawResult?.type === 'images' && tool.rawResult.images?.length"
						class="tool-execution__image"
					>
						<div v-if="tool.rawResult.text" class="tool-execution__image-caption">
							{{ tool.rawResult.text }}
						</div>
						<div class="tool-execution__image-grid">
							<div
								v-for="(img, idx) in tool.rawResult.images"
								:key="idx"
								class="tool-execution__image-card"
							>
								<el-image
									:src="resolveBackendAssetUrl(img.url)"
									fit="cover"
									:preview-src-list="tool.rawResult.images.map(i => resolveBackendAssetUrl(i.url))"
									:initial-index="idx"
									preview-teleported
									class="tool-execution__image-grid-item"
								/>
								<div
									v-if="formatToolImageItemCaption(img)"
									class="tool-execution__image-item-caption"
								>
									{{ formatToolImageItemCaption(img) }}
								</div>
							</div>
						</div>
					</div>
					<button
						v-if="tool.isLongResult"
						class="tool-execution__toggle"
						@click="toggleToolExpanded(tool.key)"
					>
						{{ isToolExpanded(tool.key) ? t('收起结果') : t('展开结果') }}
					</button>
				</div>
				</template>
			</div>

			<div class="message-item__image" v-if="messageImageUrl">
				<img :src="messageImageUrl" @click="openImage" />
			</div>

			<div class="message-item__token-stats" v-if="tokenUsage && message.role === 'assistant'">
				<span class="token-label">{{ tokenUsage.estimated ? '~' : '' }}入 {{ formatTokenCount(tokenUsage.total.input) }}</span>
				<span class="token-label">出 {{ formatTokenCount(tokenUsage.total.output) }}</span>
				<span class="token-total">总 {{ formatTokenCount(tokenUsage.total.total) }} tokens</span>
				<span class="token-calls" v-if="tokenUsage.total.apiCalls">{{ tokenUsage.total.apiCalls }} 次调用</span>
			</div>
		</div>

		<div class="message-item__time" v-if="hasVisibleBody && message.createTime">
			{{ message.createTime }}
		</div>

		<el-image-viewer
			v-if="previewVisible"
			:url-list="[previewUrl]"
			:z-index="3000"
			@close="previewVisible = false"
		/>
	</div>
</template>

<script lang="ts" setup>
defineOptions({
	name: 'message-item'
});

import { computed, ref } from 'vue';
import { Loading } from '@element-plus/icons-vue';
import { config } from '/@/config';
import { t } from '/@/plugins/i18n';
import type { ChatMessage } from '../types/index.d';
import type { TodoItem, TodoSummary } from '../types/index.d';
import ThinkingBlock from './thinking-block.vue';
import TodoCard from './todo-card.vue';
import MessageAttachments from './chat/MessageAttachments.vue';
import { projectToolRender } from '../tools/renderer-registry';

const props = defineProps<{
	message: ChatMessage;
}>();

const expandedToolResults = ref<Record<string, boolean>>({});

const tokenUsage = computed(() => {
	const meta = props.message.metadata;
	if (!meta) return null;
	const parsed = typeof meta === 'string' ? (() => { try { return JSON.parse(meta); } catch { return null; } })() : meta;
	const raw = parsed?.tokenUsage;
	if (!raw) return null;
	if (raw.total && typeof raw.total === 'object') return raw;
	if (raw.inputTokens !== undefined) {
		return { total: { input: raw.inputTokens, output: raw.outputTokens, total: raw.totalTokens, apiCalls: raw.apiCalls } };
	}
	if (raw.input !== undefined) {
		return { total: { input: raw.input, output: raw.output, total: raw.total, apiCalls: raw.apiCalls } };
	}
	return null;
});

const toolExecutions = computed(() => {
	const meta = props.message.metadata;
	if (!meta) return [];
	const parsed = typeof meta === 'string' ? (() => { try { return JSON.parse(meta); } catch { return null; } })() : meta;
	const raw = parsed?.skillExecutions;
	if (!Array.isArray(raw)) return [];
	const deduped = new Map<string, ReturnType<typeof projectToolRender>>();
	raw.forEach((item: any, index: number) => {
		const projection = projectToolRender({
			toolCallId: String(item?.toolCallId || item?.name || `tool-${index + 1}`),
			toolName: String(item?.name || item?.label || 'tool'),
			label: typeof item?.label === 'string' ? item.label : undefined,
			args: item?.args && typeof item.args === 'object' ? item.args : undefined,
			result: typeof item?.result === 'string' ? item.result : '',
			rawResult: item?.rawResult && typeof item.rawResult === 'object' ? item.rawResult : undefined,
			status: item?.status === 'done' ? 'done' : 'error',
		});
		const dedupeKey = projection.toolName === 'todo' ? '__todo__' : projection.toolCallId;
		deduped.set(dedupeKey, projection);
	});
	return [...deduped.values()];
});

function stringifyContent(value: unknown): string {
	if (typeof value === 'string') return value;
	try {
		return JSON.stringify(value, null, 2);
	} catch {
		return String(value);
	}
}

function formatTokenCount(n: number): string {
	if (!n) return '0';
	if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
	return String(n);
}

const messageImageUrl = computed(() => {
	if (props.message.role !== 'user') return null;
	const content = props.message.content;
	if (!content) return null;
	const match = content.match(/图片文件路径:\s*(.+)/);
	if (!match) return null;
	const imgPath = match[1].trim();
	if (imgPath.startsWith('http')) return imgPath;
	return `${config.baseUrl}${imgPath.startsWith('/') ? '' : '/'}${imgPath}`;
});

const displayContent = computed(() => {
	const content = props.message.content;
	if (!content || props.message.role !== 'user') return content;
	return content.replace(/\n?图片文件路径:\s*.+$/, '').trim() || content;
});

const renderedContent = computed(() => {
	const content = props.message.content;
	if (!content || props.message.role === 'user') return content;

	let processed = content.replace(
		/(?<!\]\()(?<!\()(\/(?:dev\/|api\/)?open\/agent\/workspace\/file\?[^\s)]+)/g,
		(match) => {
			const cleanPath = match.replace(/^\/(dev|api)\//, '/');
			const url = `${config.baseUrl}${cleanPath}`;
			return `[查看工作区文件](${url})`;
		}
	);

	processed = processed.replace(
		/(?<!!)(\[([^\]]*)\]\((https?:\/\/[^\s)]+)\))/g,
		(full, _link, text, url) => {
			if (isImageUrl(url)) {
				return `![${text || '生成图片'}](${url})`;
			}
			return full;
		}
	);

	processed = processed.replace(
		/(?<!\(|!\[|\])\b(https?:\/\/[^\s)]+)/g,
		(full, url) => {
			if (isImageUrl(url)) {
				return `![生成图片](${url})`;
			}
			return full;
		}
	);

	return processed;
});

function isImageUrl(url: string): boolean {
	const imageCdnPatterns = ['byteimg.com', 'volccdn.com', 'imagex.cn', 'tosv.byted.org'];
	if (imageCdnPatterns.some(d => url.includes(d))) return true;
	if (url.includes('volces.com') && url.includes('/images/')) return true;
	const pathname = url.split('?')[0];
	return /\.(png|jpe?g|webp|gif|bmp|svg)$/i.test(pathname);
}

function onContentClick(e: MouseEvent) {
	const target = e.target as HTMLElement;
	if (target.tagName === 'IMG') {
		e.preventDefault();
		e.stopPropagation();
		const imgSrc = (target as HTMLImageElement).src;
		if (imgSrc) {
			previewUrl.value = imgSrc;
			previewVisible.value = true;
		}
		return;
	}
	const anchor = target.closest('a');
	if (anchor && anchor.href) {
		e.preventDefault();
		window.open(anchor.href, '_blank');
	}
}

const previewVisible = ref(false);
const previewUrl = ref('');

const hasVisibleBody = computed(() => {
	if (props.message.role === 'clarify') return false;
	const meta = props.message.metadata;
	const parsed = meta && typeof meta === 'string' ? (() => { try { return JSON.parse(meta); } catch { return null; } })() : meta;
	const hasAttachments = Array.isArray(parsed?.attachments) && parsed.attachments.length > 0;
	return Boolean(
		props.message.skillName ||
		(props.message.role === 'assistant' && props.message.thinking) ||
		props.message.content ||
		toolExecutions.value.length ||
		messageImageUrl.value ||
		(tokenUsage.value && props.message.role === 'assistant') ||
		(props.message.role === 'user' && hasAttachments)
	);
});

function openImage() {
	if (messageImageUrl.value) {
		window.open(messageImageUrl.value, '_blank');
	}
}

function openToolImage(url: string) {
	previewUrl.value = resolveBackendAssetUrl(url);
	previewVisible.value = true;
}

function resolveBackendAssetUrl(url?: string): string {
	if (!url) return '';
	if (/^(https?:)?\/\//i.test(url) || url.startsWith('data:') || url.startsWith('blob:')) {
		return url;
	}
	if (url.startsWith('/workspace/') || url.startsWith('/upload/') || url.startsWith('/open/')) {
		return `${String(config.baseUrl || '').replace(/\/$/, '')}${url}`;
	}
	return url;
}

function formatToolImageCaption(rawResult: any): string {
	const parts: string[] = [];
	if (rawResult.mimeType) parts.push(rawResult.mimeType);
	if (rawResult.width && rawResult.height) parts.push(`${rawResult.width}x${rawResult.height}`);
	if (rawResult.bytes) parts.push(formatBytes(rawResult.bytes));
	if (rawResult.resized && rawResult.originalWidth && rawResult.originalHeight) {
		parts.push(`原图 ${rawResult.originalWidth}x${rawResult.originalHeight}`);
	}
	return parts.join(' · ');
}

function formatToolImageItemCaption(image: any): string {
	const parts: string[] = [];
	if (image?.purpose) parts.push(String(image.purpose));
	if (image?.label) parts.push(String(image.label));
	if (typeof image?.timestamp === 'number' && Number.isFinite(image.timestamp)) {
		parts.push(`${image.timestamp.toFixed(2).replace(/\.00$/, '')}s`);
	}
	if (image?.reason) parts.push(String(image.reason));
	return parts.join(' · ');
}

function formatBytes(bytes: number): string {
	if (!Number.isFinite(bytes) || bytes <= 0) return '0 B';
	if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
	if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
	return `${bytes} B`;
}

function toggleToolExpanded(key: string) {
	expandedToolResults.value = {
		...expandedToolResults.value,
		[key]: !expandedToolResults.value[key],
	};
}

function isToolExpanded(key: string) {
	return Boolean(expandedToolResults.value[key]);
}

function parseTodoResult(result?: string): { items: TodoItem[]; summary: TodoSummary | null } | null {
	if (!result) return null;
	try {
		const parsed = JSON.parse(result);
		const todos = parsed?.todos || parsed?.items;
		if (!Array.isArray(todos) || !todos.length) return null;
		const items: TodoItem[] = todos.map((t: any, i: number) => ({
			id: String(t.id ?? i),
			content: String(t.content || t.title || t.text || ''),
			status: t.status || 'pending',
		}));
		return { items, summary: parsed?.summary ?? null };
	} catch {
		return null;
	}
}
</script>

<style lang="scss" scoped>
.message-item {
	display: flex; flex-direction: column; margin-bottom: 20px;
	animation: msg-enter 0.3s ease-out;

	&__inner {
		border-radius: 14px; padding: 12px 16px; max-width: var(--chat-content-max-width, 85%);
		word-break: break-word; position: relative;

		.message-item__desc {
			.tool {
				display: inline-flex; align-items: center; gap: 6px; font-size: 12px;
				max-width: 100%;
				white-space: nowrap; margin-bottom: 10px;
				padding: 4px 10px;
				border-radius: 999px;
				background: var(--el-color-primary-light-9);
				border: 1px solid var(--el-color-primary-light-7);
				color: var(--el-color-primary-dark-2);
				overflow: hidden;
				text-overflow: ellipsis;
				.is-loading { margin-left: 5px; color: var(--el-color-primary); }
			}
			& + .message-item__content { margin-top: 10px; }
		}

		.message-item__content {
			font-size: 14px; line-height: 1.7;
			:deep(img) { max-width: 360px; max-height: 360px; border-radius: 10px; cursor: pointer; object-fit: contain; border: 1px solid var(--el-border-color-lighter); display: block; margin: 8px 0; transition: opacity 0.2s; &:hover { opacity: 0.85; } }
		}

		.message-item__image {
			margin-top: 8px;
			img { max-width: 200px; max-height: 150px; border-radius: 8px; cursor: pointer; object-fit: cover; border: 1px solid rgba(255, 255, 255, 0.2); transition: transform 0.2s; &:hover { transform: scale(1.02); } }
		}
		.message-item__tool-executions {
			display: grid;
			gap: 10px;
			margin-top: 12px;
			padding-top: 12px;
			border-top: 1px solid var(--el-border-color-extra-light);
		}
		.tool-execution {
			padding: 10px 12px;
			border-radius: 12px;
			background: rgba(24, 54, 38, 0.05);
			border: 1px solid rgba(24, 54, 38, 0.08);

			&.is-error {
				background: rgba(194, 65, 12, 0.06);
				border-color: rgba(194, 65, 12, 0.16);
			}
		}
		.tool-execution__head {
			display: flex;
			align-items: center;
			justify-content: space-between;
			gap: 8px;
			font-size: 12px;
			font-weight: 700;
			color: var(--el-text-color-primary);
		}
		.tool-execution__status {
			color: var(--el-text-color-secondary);
		}
		.tool-execution__meta {
			display: flex;
			flex-wrap: wrap;
			gap: 6px;
			margin-top: 8px;
		}
		.tool-execution__meta-pill {
			display: inline-flex;
			align-items: center;
			max-width: 100%;
			overflow: hidden;
			border-radius: 999px;
			border: 1px solid var(--el-border-color-lighter);
			background: var(--el-fill-color-blank);
			font-size: 11px;
			line-height: 1.5;

			&.is-log {
				border-color: var(--el-color-warning-light-5);
				background: var(--el-color-warning-light-9);
			}

			&.is-offset {
				border-color: var(--el-color-primary-light-7);
				background: var(--el-color-primary-light-9);
			}
		}
		.tool-execution__meta-label {
			padding: 2px 6px;
			color: var(--el-text-color-secondary);
			background: rgba(15, 23, 42, 0.04);
			border-right: 1px solid var(--el-border-color-lighter);
			white-space: nowrap;
		}
		.tool-execution__meta-value {
			padding: 2px 8px;
			color: var(--el-text-color-primary);
			overflow: hidden;
			text-overflow: ellipsis;
			white-space: nowrap;
			max-width: min(520px, 72vw);
		}
		.tool-execution__args,
		.tool-execution__result {
			margin: 8px 0 0;
			white-space: pre-wrap;
			font-family: Consolas, Monaco, monospace;
			font-size: 12px;
			line-height: 1.6;
			color: var(--el-text-color-regular);
		}

		.tool-execution__args {
			padding: 8px;
			border-radius: 8px;
			background: rgba(15, 23, 42, 0.04);
			color: var(--el-text-color-secondary);
		}

		.tool-execution__result {
			&.is-collapsed {
				max-height: 118px;
				overflow: hidden;
				mask-image: linear-gradient(180deg, #000 0%, #000 68%, transparent 100%);
			}
		}
		.tool-execution__toggle {
			margin-top: 8px;
			padding: 0;
			border: none;
			background: transparent;
			color: var(--el-color-primary);
			font-size: 12px;
			font-weight: 600;
			cursor: pointer;
		}
		.tool-execution__image {
			margin-top: 8px;
			img {
				max-width: 280px;
				max-height: 220px;
				border-radius: 10px;
				cursor: pointer;
				border: 1px solid var(--el-border-color-lighter);
				background: var(--el-fill-color-blank);
				object-fit: contain;
			}
		}
		.tool-execution__image-caption {
			margin-top: 6px;
			font-size: 11px;
			line-height: 1.5;
			color: var(--el-text-color-secondary);
		}
		.tool-execution__image-single {
			max-width: 360px;
			border-radius: 8px;
		}
		.tool-execution__image-grid {
			display: grid;
			grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
			gap: 8px;
			margin-top: 8px;
		}
		.tool-execution__image-card {
			min-width: 0;
		}
		.tool-execution__image-grid-item {
			width: 100%;
			aspect-ratio: 1;
			border-radius: 8px;
			cursor: pointer;
			object-fit: cover;
		}
		.tool-execution__image-item-caption {
			margin-top: 4px;
			color: var(--el-text-color-secondary);
			font-size: 11px;
			line-height: 1.35;
			word-break: break-word;
		}
		.message-item__token-stats {
			display: flex; align-items: center; gap: 8px; margin-top: 10px;
			padding-top: 8px; border-top: 1px solid var(--el-border-color-extra-light);
			font-size: 11px; color: var(--el-text-color-placeholder); font-variant-numeric: tabular-nums;
			.token-total { font-weight: 500; color: var(--el-text-color-secondary); }
			.token-calls { font-size: 10px; }
		}
	}

	&__time { font-size: 11px; color: var(--el-text-color-placeholder); margin-top: 4px; }

	&.is-right {
		align-items: flex-end;
		.message-item__inner {
			background: var(--el-color-primary); color: #fff;
			box-shadow: 0 2px 8px rgba(0,0,0,0.1); border-bottom-right-radius: 4px;
		}
		.message-item__time { text-align: right; }
		.message-item__image img { border-color: rgba(255, 255, 255, 0.3); }
	}

	&.is-left {
		align-items: flex-start;
		.message-item__inner {
			width: 100%; background: var(--el-fill-color-light);
			border: 1px solid var(--el-border-color-extra-light); border-bottom-left-radius: 4px;
		}
	}
}

@keyframes msg-enter { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
</style>
