import { computed, ref } from 'vue';
import { defineStore } from 'pinia';
import { config } from '/@/config';
import { useNetaClawWS } from '../hooks/websocket';
import type {
	AgentInfo,
	AgentSession,
	RuntimeProcessEventProjection,
	RuntimeProcessLogRef,
	SubagentEvidenceSummary,
	SubagentProcessEvent,
	SubagentProjectionDiagnostics,
	SubagentRuntimeState,
	SubagentTaskPanel,
	SessionTreeEntry,
	SessionTreeSubagentBatchContent,
	SessionTreeSubagentResultContent,
	SessionTreeSessionMeta,
	SessionTreeSnapshot,
	StreamingToolProjection,
	ToolRuntimeRouteProjection,
	WSServerEvent,
} from '../types/index.d';
import { projectToolRender } from '../tools/renderer-registry';
import {
	extractSubagentEvidenceSummariesByEntry,
	extractSubagentProjectionDiagnosticsByEntry,
	extractSubagentProcessEventsByEntry,
	extractSubagentTaskPanelsByEntry,
} from './subagent_projection';
import {
	buildSubagentRuntimeMap,
	extractToolRuntimeRoutesByBatch,
	getNormalizedSubagentBatchMetadata,
} from './subagent_batch_runtime';
import { canDeleteAgentSession, isAuditOrderSessionMetadata } from '../utils/session';

const BASE_OPEN = '/open/netaclaw';
const LAST_TREE_SESSION_STORAGE_KEY = 'neta:agent-chat:last-tree-session';
const LAST_AGENT_STORAGE_KEY = 'neta:agent-chat:last-agent';

let wsInstance: ReturnType<typeof useNetaClawWS> | null = null;
let wsCleanup: (() => void) | null = null;

function getStoredSessionId() {
	try {
		return localStorage.getItem(LAST_TREE_SESSION_STORAGE_KEY) || '';
	} catch {
		return '';
	}
}

function setStoredSessionId(sessionId: string) {
	try {
		if (sessionId) localStorage.setItem(LAST_TREE_SESSION_STORAGE_KEY, sessionId);
		else localStorage.removeItem(LAST_TREE_SESSION_STORAGE_KEY);
	} catch {
		// ignore
	}
}

function getStoredAgentId(): number | null {
	try {
		const raw = localStorage.getItem(LAST_AGENT_STORAGE_KEY);
		if (!raw) return null;
		const parsed = Number(raw);
		return Number.isNaN(parsed) ? null : parsed;
	} catch {
		return null;
	}
}

function setStoredAgentId(agentId: number | null) {
	try {
		if (agentId == null) localStorage.removeItem(LAST_AGENT_STORAGE_KEY);
		else localStorage.setItem(LAST_AGENT_STORAGE_KEY, String(agentId));
	} catch {
		// ignore
	}
}

async function shouldRestoreLastSession(agentId: number | null): Promise<boolean> {
	if (!agentId) return true;
	try {
		const resp = await fetch(`${config.baseUrl}${BASE_OPEN}/agent/info`, {
			method: 'POST',
			headers: { 'Content-Type': 'application/json' },
			body: JSON.stringify({ id: agentId }),
		});
		const data = await resp.json();
		const rawConfig = data?.data?.config;
		const parsedConfig =
			typeof rawConfig === 'string'
				? JSON.parse(rawConfig || '{}')
				: (rawConfig || {});
		return parsedConfig?.session?.restoreLastSession !== false;
	} catch {
		return true;
	}
}

function buildFallbackTitle(session: SessionTreeSessionMeta) {
	const title = session.title?.trim();
	if (title && title !== 'default') return title;
	const sid = session.sessionId;
	const groupMatch = /^channel:\d+:weixin:group:(.+)$/.exec(sid);
	if (groupMatch) {
		const roomId = groupMatch[1];
		return `微信群 · ${roomId.slice(-8)}`;
	}
	const dmMatch = /^channel:\d+:weixin:(.+)$/.exec(sid);
	if (dmMatch) {
		return `微信 · ${dmMatch[1].slice(-6)}`;
	}
	return `会话 ${sid.slice(0, 8)}`;
}

function toStreamingToolsFromEntry(entry: SessionTreeEntry | null | undefined): StreamingToolProjection[] {
	const raw = entry?.message?.metadata?.skillExecutions;
	if (!Array.isArray(raw)) return [];
	const deduped = new Map<string, StreamingToolProjection>();
	for (let index = 0; index < raw.length; index += 1) {
		const item = raw[index];
		if (!item || typeof item !== 'object') continue;
		const projected = 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,
			result: typeof item.result === 'string' ? item.result : undefined,
			status: item.status === 'done' ? 'done' : 'error',
		});
		const processEvents = normalizeToolProcessEvents(item.processEvents);
		const latestProcessEvent = normalizeToolProcessEvent(item.latestProcessEvent)
			|| (processEvents?.length ? processEvents[processEvents.length - 1] : undefined);
		const dedupeKey = projected.toolName === 'todo' ? `__todo__` : projected.toolCallId;
		deduped.set(dedupeKey, {
			toolCallId: projected.toolCallId,
			toolName: projected.toolName,
			label: projected.label,
			result: projected.result,
			rawResult: item.rawResult && typeof item.rawResult === 'object' ? item.rawResult : undefined,
			latestProcessEvent,
			processEvents,
			processLogRef: normalizeProcessLogRef(item.processLogRef),
			status: projected.status,
		});
	}
	return [...deduped.values()];
}

export const useAgentChatStore = defineStore('agent-chat', () => {
	const initialized = ref(false);
	const loading = ref(false);
	const agents = ref<AgentInfo[]>([]);
	const sessions = ref<AgentSession[]>([]);
	const currentAgentId = ref<number | null>(null);
	const sessionId = ref('');
	const selectedEntryId = ref<string | null>(null);
	const composerPrefill = ref('');
	const switchingLeafEntryId = ref<string | null>(null);
	const pendingLeafConfirmation = ref<{ entryId: string; previousLeafEntryId: string | null } | null>(null);
	const pendingUserContent = ref('');
	const pendingClarify = ref<{ requestId: string; question: string; choices: string[] } | null>(null);
	const pendingToolConfirm = ref<{
		requestId: string;
		toolName: string;
		toolLabel: string;
		args: Record<string, unknown>;
		riskReason: string;
	} | null>(null);

	const sessionMeta = ref<SessionTreeSessionMeta | null>(null);
	const entries = ref<SessionTreeEntry[]>([]);
	const entryById = ref<Record<string, SessionTreeEntry>>({});
	const childrenByParentId = ref<Record<string, string[]>>({});
	const activePathIds = ref<string[]>([]);
	const subagentRuntimeByBatchId = ref<Record<string, SubagentRuntimeState>>({});
	const toolRuntimeRoutesByBatchId = ref<Record<string, ToolRuntimeRouteProjection[]>>({});
	const subagentEvidenceSummariesByEntryId = ref<Record<string, SubagentEvidenceSummary[]>>({});
	const subagentProcessEventsByEntryId = ref<Record<string, SubagentProcessEvent[]>>({});
	const subagentTaskPanelsByEntryId = ref<Record<string, SubagentTaskPanel[]>>({});
	const subagentProjectionDiagnosticsByEntryId = ref<Record<string, SubagentProjectionDiagnostics>>({});

	const streamingAssistantEntryId = ref<string | null>(null);
	const streamingAssistantText = ref('');
	const streamingAssistantThinking = ref('');
	const streamingTools = ref<StreamingToolProjection[]>([]);
	const streamingError = ref('');

	const currentAgent = computed(() => {
		return agents.value.find(item => item.id === currentAgentId.value) || null;
	});

	const selectedEntry = computed(() => {
		if (!selectedEntryId.value) return null;
		return entryById.value[selectedEntryId.value] || null;
	});

	const activePathEntries = computed(() => {
		return activePathIds.value
			.map(id => entryById.value[id])
			.filter((entry): entry is SessionTreeEntry => Boolean(entry));
	});

	const visibleConversationEntries = computed(() => {
		return activePathEntries.value.filter(entry => {
			if (entry.type === 'message') return true;
			if (entry.type === 'branch_summary') return true;
			if (entry.type === 'compaction') return true;
			if (entry.type === 'custom_message') return entry.display !== false;
			return entry.type === 'subagent_batch' || entry.type === 'subagent_result';
		});
	});

	const treeRoots = computed(() => childrenByParentId.value.__root__ || []);

	const canSend = computed(() => {
		if (pendingClarify.value) return true;
		return !loading.value;
	});

	function initWS() {
		if (wsInstance) return;
		wsInstance = useNetaClawWS();
		wsInstance.connect();
		wsCleanup = wsInstance.onEvent(handleWSEvent);
	}

	async function loadAgents() {
		try {
			const resp = await fetch(`${config.baseUrl}${BASE_OPEN}/agent/list`, {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({}),
			});
			const data = await resp.json();
			if (data.code === 1000 && Array.isArray(data.data)) {
				agents.value = data.data;
			}
		} catch {
			// ignore
		}
	}

	async function loadSessions() {
		try {
			const resp = await fetch(`${config.baseUrl}${BASE_OPEN}/session/tree/list`, {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ agentId: currentAgentId.value || undefined }),
			});
			const data = await resp.json();
			if (data.code === 1000 && Array.isArray(data.data)) {
				sessions.value = data.data.map((item: any) => ({
					id: item.id,
					sessionId: item.sessionId,
					agentId: item.agentId ? Number(item.agentId) : null,
					title: buildFallbackTitle(item),
					status: item.status === 'active' ? 1 : 0,
					metadata: item.metadata,
					createTime: item.createTime,
					updateTime: item.updateTime,
				}));
			}
		} catch {
			// ignore
		}
	}

	function applySnapshot(snapshot: SessionTreeSnapshot) {
		sessionMeta.value = snapshot.session;
		if (pendingLeafConfirmation.value?.entryId && snapshot.session.leafEntryId === pendingLeafConfirmation.value.entryId) {
			pendingLeafConfirmation.value = null;
		}
		entries.value = snapshot.entries;
		entryById.value = snapshot.entries.reduce<Record<string, SessionTreeEntry>>((acc, entry) => {
			acc[entry.id] = entry;
			return acc;
		}, {});
		childrenByParentId.value = snapshot.childrenByParentId || {};
		activePathIds.value = snapshot.activePath.map(entry => entry.id);
		if (!selectedEntryId.value || !entryById.value[selectedEntryId.value]) {
			selectedEntryId.value = snapshot.session.leafEntryId
				|| snapshot.activePath[snapshot.activePath.length - 1]?.id
				|| null;
		}
		streamingAssistantEntryId.value = null;
		pendingUserContent.value = '';
		streamingAssistantText.value = '';
		streamingAssistantThinking.value = '';
		streamingTools.value = [];
		streamingError.value = '';
		subagentRuntimeByBatchId.value = buildSubagentRuntimeMap(snapshot.entries);
		toolRuntimeRoutesByBatchId.value = extractToolRuntimeRoutesByBatch(snapshot.entries);
		subagentEvidenceSummariesByEntryId.value = extractSubagentEvidenceSummariesByEntry(snapshot.entries);
		subagentProcessEventsByEntryId.value = extractSubagentProcessEventsByEntry(snapshot.entries);
		subagentTaskPanelsByEntryId.value = extractSubagentTaskPanelsByEntry(snapshot.entries);
		subagentProjectionDiagnosticsByEntryId.value = extractSubagentProjectionDiagnosticsByEntry(snapshot.entries);
	}

	function upsertEntry(entry: SessionTreeEntry) {
		const existingIndex = entries.value.findIndex(item => item.id === entry.id);
		if (existingIndex >= 0) {
			entries.value.splice(existingIndex, 1, entry);
		} else {
			entries.value.push(entry);
		}
		entryById.value = {
			...entryById.value,
			[entry.id]: entry,
		};
		const parentKey = entry.parentId ?? '__root__';
		const existingChildren = childrenByParentId.value[parentKey] || [];
		if (!existingChildren.includes(entry.id)) {
			childrenByParentId.value = {
				...childrenByParentId.value,
				[parentKey]: [...existingChildren, entry.id],
			};
		}
		if (entry.type === 'subagent_batch' || entry.type === 'subagent_result') {
			syncSubagentRuntimeFromEntries();
		}
		if (entry.type === 'subagent_result' || entry.type === 'message') {
			syncSubagentResultProjectionsFromEntries();
		}
	}

	function syncSubagentRuntimeFromEntries() {
		subagentRuntimeByBatchId.value = mergeSubagentRuntimeWithCanonical(
			subagentRuntimeByBatchId.value,
			buildSubagentRuntimeMap(entries.value),
		);
		toolRuntimeRoutesByBatchId.value = {
			...toolRuntimeRoutesByBatchId.value,
			...extractToolRuntimeRoutesByBatch(entries.value),
		};
	}

	function syncSubagentResultProjectionsFromEntries() {
		subagentEvidenceSummariesByEntryId.value = extractSubagentEvidenceSummariesByEntry(entries.value);
		subagentProcessEventsByEntryId.value = extractSubagentProcessEventsByEntry(entries.value);
		subagentTaskPanelsByEntryId.value = extractSubagentTaskPanelsByEntry(entries.value);
		subagentProjectionDiagnosticsByEntryId.value = extractSubagentProjectionDiagnosticsByEntry(entries.value);
	}

	function rebuildActivePath(leafEntryId: string | null | undefined) {
		if (!leafEntryId) {
			activePathIds.value = [];
			return;
		}
		activePathIds.value = getPathToEntry(leafEntryId).map(entry => entry.id);
	}

	function applySessionMeta(meta: SessionTreeSessionMeta | undefined) {
		if (!meta) return;
		sessionMeta.value = meta;
		if (meta.sessionId) {
			sessionId.value = meta.sessionId;
			setStoredSessionId(meta.sessionId);
		}
	}

	async function loadSnapshot(nextSessionId: string) {
		const resp = await fetch(`${config.baseUrl}${BASE_OPEN}/session/tree/snapshot`, {
			method: 'POST',
			headers: { 'Content-Type': 'application/json' },
			body: JSON.stringify({
				sessionId: nextSessionId,
				agentId: currentAgentId.value || undefined,
			}),
		});
		const data = await resp.json();
		if (data.code === 1000 && data.data) {
			applySnapshot(data.data);
			sessionId.value = nextSessionId;
			if (data.data?.session?.agentId != null) {
				const parsedAgentId = Number(data.data.session.agentId);
				if (!Number.isNaN(parsedAgentId)) {
					currentAgentId.value = parsedAgentId;
					setStoredAgentId(parsedAgentId);
				}
			}
			setStoredSessionId(nextSessionId);
			return;
		}
		throw new Error(data?.message || `Failed to load session snapshot: ${nextSessionId}`);
	}

	function resetConversationState() {
		sessionId.value = '';
		sessionMeta.value = null;
		entries.value = [];
		entryById.value = {};
		childrenByParentId.value = {};
		activePathIds.value = [];
		selectedEntryId.value = null;
		composerPrefill.value = '';
		switchingLeafEntryId.value = null;
		pendingLeafConfirmation.value = null;
		streamingAssistantEntryId.value = null;
		pendingUserContent.value = '';
		streamingAssistantText.value = '';
		streamingAssistantThinking.value = '';
		streamingTools.value = [];
		streamingError.value = '';
		subagentRuntimeByBatchId.value = {};
		toolRuntimeRoutesByBatchId.value = {};
		subagentEvidenceSummariesByEntryId.value = {};
		subagentProcessEventsByEntryId.value = {};
		subagentTaskPanelsByEntryId.value = {};
		subagentProjectionDiagnosticsByEntryId.value = {};
		loading.value = false;
		setStoredSessionId('');
	}

	function newSession() {
		resetConversationState();
	}

	async function switchSession(nextSessionId: string) {
		resetConversationState();
		await loadSnapshot(nextSessionId);
	}

	async function selectAgent(agentId: number | null) {
		currentAgentId.value = agentId;
		setStoredAgentId(agentId);
		if (!(await shouldRestoreLastSession(agentId))) {
			setStoredSessionId('');
		}
		resetConversationState();
		await loadSessions();
	}

	function selectTreeEntry(entryId: string) {
		selectedEntryId.value = entryId;
		composerPrefill.value = '';
	}

	function getPathToEntry(entryId: string | null) {
		if (!entryId) return [];
		const path: SessionTreeEntry[] = [];
		const seen = new Set<string>();
		let current: SessionTreeEntry | undefined = entryById.value[entryId];
		while (current) {
			if (seen.has(current.id)) break;
			seen.add(current.id);
			path.unshift(current);
			current = current.parentId ? entryById.value[current.parentId] : undefined;
		}
		return path;
	}

	async function continueFromEntry(entryId: string) {
		if (!sessionId.value) {
			return { success: false as const, fallback: false, error: '当前没有可切换的会话。' };
		}
		const previousLeafEntryId = sessionMeta.value?.leafEntryId || null;
		switchingLeafEntryId.value = entryId;
		try {
			const resp = await fetch(`${config.baseUrl}${BASE_OPEN}/session/tree/switchLeaf`, {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({
					sessionId: sessionId.value,
					leafEntryId: entryId,
					agentId: currentAgentId.value || undefined,
				}),
			});
			const data = await resp.json();
			if (data.code === 1000 && data.data) {
				applySnapshot(data.data);
				selectedEntryId.value = entryId;
				pendingLeafConfirmation.value = null;
				return { success: true as const, fallback: false };
			} else {
				const path = getPathToEntry(entryId);
				activePathIds.value = path.map(entry => entry.id);
				selectedEntryId.value = entryId;
				pendingLeafConfirmation.value = {
					entryId,
					previousLeafEntryId,
				};
				return { success: true as const, fallback: true, error: data?.message || null };
			}
		} catch (error) {
			return {
				success: false as const,
				fallback: false,
				error: error instanceof Error ? error.message : '切换失败',
			};
		} finally {
			switchingLeafEntryId.value = null;
		}
	}

	async function sendMessage(content: string, attachments?: import('../types/index.d').ChatAttachment[]) {
		const trimmed = content.trim();
		if (!trimmed || loading.value) return;
		initWS();
		loading.value = true;
		pendingUserContent.value = trimmed;
		streamingAssistantText.value = '';
		streamingAssistantThinking.value = '';
		streamingTools.value = [];
		streamingError.value = '';

		let targetSessionId = sessionId.value;
		if (!targetSessionId) {
			targetSessionId = `temp-${Date.now()}`;
			sessionId.value = targetSessionId;
		}

		wsInstance?.send({
			type: 'chat',
			sessionId: targetSessionId,
			content: trimmed,
			agentId: currentAgentId.value || undefined,
			...(attachments?.length ? { attachments } : {}),
		} as any);
	}

	function sendClarifyResponse(requestId: string, answer: string) {
		if (!wsInstance) return;
		wsInstance.send({
			type: 'clarify_response',
			sessionId: sessionId.value,
			requestId,
			answer,
		} as any);
		pendingClarify.value = null;
	}

	function sendToolConfirmResponse(requestId: string, confirmed: boolean) {
		if (!wsInstance) return;
		wsInstance.send({
			type: 'tool_confirmation_response',
			sessionId: sessionId.value,
			requestId,
			confirmed,
		} as any);
		pendingToolConfirm.value = null;
	}

	async function deleteSession(sessionIdToDelete: string) {
		try {
			const sessionToDelete = sessions.value.find(item => item.sessionId === sessionIdToDelete);
			if (!canDeleteAgentSession(sessionToDelete)) {
				console.warn(`Audit order session cannot be deleted: ${sessionIdToDelete}`);
				return;
			}
			if (sessionId.value === sessionIdToDelete && isAuditOrderSessionMetadata(sessionMeta.value?.metadata)) {
				console.warn(`Audit order session cannot be deleted: ${sessionIdToDelete}`);
				return;
			}
			const deleteAgentId = sessionToDelete?.agentId ?? currentAgentId.value ?? undefined;
			const resp = await fetch(`${config.baseUrl}${BASE_OPEN}/session/delete`, {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ sessionId: sessionIdToDelete, agentId: deleteAgentId }),
			});
			const data = await resp.json();
			if (!resp.ok || data?.code !== 1000) {
				throw new Error(data?.message || `Failed to delete session: ${sessionIdToDelete}`);
			}
			sessions.value = sessions.value.filter(item => item.sessionId !== sessionIdToDelete);
			if (sessionId.value === sessionIdToDelete) {
				resetConversationState();
			} else if (getStoredSessionId() === sessionIdToDelete) {
				setStoredSessionId('');
			}
		} catch (error) {
			console.error(error);
		}
	}

	async function deleteAllSessions() {
		try {
			const resp = await fetch(`${config.baseUrl}${BASE_OPEN}/session/deleteAll`, {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({}),
			});
			const data = await resp.json();
			if (!resp.ok || data?.code !== 1000) {
				throw new Error(data?.message || 'Failed to delete all sessions');
			}
			sessions.value = [];
			resetConversationState();
		} catch (error) {
			console.error(error);
		}
	}

	async function refreshCurrentSnapshot() {
		if (!sessionId.value) return;
		await loadSnapshot(sessionId.value);
	}

	function upsertStreamingTool(tool: StreamingToolProjection) {
		const existingIndex = streamingTools.value.findIndex(item => item.toolCallId === tool.toolCallId);
		if (existingIndex >= 0) {
			streamingTools.value.splice(existingIndex, 1, {
				...streamingTools.value[existingIndex],
				...tool,
				processEvents: tool.processEvents || streamingTools.value[existingIndex].processEvents,
				latestProcessEvent: tool.latestProcessEvent || streamingTools.value[existingIndex].latestProcessEvent,
				processLogRef: tool.processLogRef || streamingTools.value[existingIndex].processLogRef,
			});
			return;
		}
		streamingTools.value.push(tool);
	}

	function fallbackToolCallId(event: WSServerEvent) {
		return event.toolCallId || `${event.toolName || 'tool'}-${streamingTools.value.length + 1}`;
	}

	function handleWSEvent(event: WSServerEvent) {
		if (!sessionId.value && event.sessionId) {
			sessionId.value = event.sessionId;
			setStoredSessionId(event.sessionId);
		}

		switch (event.type) {
			case 'session_entry_added': {
				const entry = event.data?.entry as SessionTreeEntry | undefined;
				if (entry) {
					upsertEntry(entry);
					if (entry.type === 'message' && entry.message?.role === 'user' && pendingUserContent.value) {
						pendingUserContent.value = '';
					}
				}
				applySessionMeta(event.data?.session);
				if (event.data?.session?.leafEntryId) {
					rebuildActivePath(event.data.session.leafEntryId);
					selectedEntryId.value = event.data.session.leafEntryId;
					if (pendingLeafConfirmation.value?.entryId === event.data.session.leafEntryId) {
						pendingLeafConfirmation.value = null;
					}
				}
				break;
			}
			case 'session_entry_updated': {
				const entry = event.data?.entry as SessionTreeEntry | undefined;
				if (entry) {
					upsertEntry(entry);
					if (loading.value && (entry.id === streamingAssistantEntryId.value || entry.id === event.data?.session?.leafEntryId)) {
						streamingTools.value = toStreamingToolsFromEntry(entry);
					}
				}
				applySessionMeta(event.data?.session);
				if (event.data?.session?.leafEntryId) {
					rebuildActivePath(event.data.session.leafEntryId);
					if (pendingLeafConfirmation.value?.entryId === event.data.session.leafEntryId) {
						pendingLeafConfirmation.value = null;
					}
				}
				break;
			}
			case 'session_leaf_changed':
				applySessionMeta(event.data?.session);
				rebuildActivePath(event.data?.leafEntryId ?? event.data?.session?.leafEntryId ?? null);
				selectedEntryId.value = event.data?.leafEntryId ?? null;
				if (pendingLeafConfirmation.value?.entryId === (event.data?.leafEntryId ?? event.data?.session?.leafEntryId ?? null)) {
					pendingLeafConfirmation.value = null;
				}
				break;
			case 'assistant_stream_start':
				streamingAssistantEntryId.value = event.data?.assistantEntryId || null;
				streamingAssistantText.value = '';
				streamingAssistantThinking.value = '';
				streamingTools.value = [];
				streamingError.value = '';
				break;
			case 'assistant_stream_delta':
				if (event.data?.assistantEntryId) {
					streamingAssistantEntryId.value = event.data.assistantEntryId;
				}
				streamingAssistantText.value += event.data?.text || '';
				break;
			case 'assistant_stream_end':
				if (event.data?.assistantEntryId) {
					streamingAssistantEntryId.value = event.data.assistantEntryId;
				}
				break;
			case 'token':
				if (!streamingAssistantEntryId.value) {
					streamingAssistantText.value += event.content || '';
				}
				break;
			case 'thinking_delta':
				streamingAssistantThinking.value += event.data?.text || '';
				break;
			case 'subagent_run_started':
				recordSubagentStarted(event);
				break;
			case 'subagent_event':
				recordSubagentEvent(event);
				break;
			case 'subagent_run_completed':
				recordSubagentCompleted(event);
				break;
			case 'tool_call_started':
				if (event.data?.toolCallId) {
					upsertStreamingTool({
						toolCallId: event.data.toolCallId,
						assistantEntryId: event.data.assistantEntryId,
						toolName: event.data.toolName,
						label: event.data.toolLabel,
						args: event.data.args,
						runtimeRoute: event.data.runtimeRoute,
						workerRoutingHint: event.data.workerRoutingHint,
						blockedReason: event.data.blockedReason,
						policySources: Array.isArray(event.data.policySources) ? event.data.policySources : undefined,
						status: event.data.status || 'running',
					});
				}
				break;
			case 'tool_result_streamed':
				if (event.data?.toolCallId) {
					const existing = streamingTools.value.find(item => item.toolCallId === event.data.toolCallId);
					upsertStreamingTool({
						toolCallId: event.data.toolCallId,
						assistantEntryId: event.data.assistantEntryId,
						toolName: event.data.toolName,
						label: event.data.toolLabel,
						result: event.data.result,
						runtimeRoute: event.data.runtimeRoute,
						workerRoutingHint: event.data.workerRoutingHint,
						blockedReason: event.data.blockedReason,
						policySources: Array.isArray(event.data.policySources) ? event.data.policySources : undefined,
						rawResult: normalizeToolRawResult(event.data.rawResult),
						processEvents: existing?.processEvents,
						latestProcessEvent: existing?.latestProcessEvent,
						processLogRef: existing?.processLogRef,
						status: 'running',
					});
				}
				break;
			case 'tool_call_completed':
				if (event.data?.toolCallId) {
					const existing = streamingTools.value.find(item => item.toolCallId === event.data.toolCallId);
					upsertStreamingTool({
						toolCallId: event.data.toolCallId,
						assistantEntryId: event.data.assistantEntryId,
						toolName: event.data.toolName,
						label: event.data.toolLabel,
						result: event.data.result,
						runtimeRoute: event.data.runtimeRoute,
						workerRoutingHint: event.data.workerRoutingHint,
						blockedReason: event.data.blockedReason,
						policySources: Array.isArray(event.data.policySources) ? event.data.policySources : undefined,
						rawResult: normalizeToolRawResult(event.data.rawResult),
						processEvents: existing?.processEvents,
						latestProcessEvent: existing?.latestProcessEvent,
						processLogRef: existing?.processLogRef,
						status: event.data.status || 'done',
					});
				}
				break;
			case 'tool_process_event':
				if (event.data?.toolCallId) {
					const processEvent = normalizeToolProcessEvent(event.data);
					if (processEvent) {
						const existing = streamingTools.value.find(item => item.toolCallId === event.data.toolCallId);
						const processEvents = [...(existing?.processEvents || []), processEvent].slice(-50);
						upsertStreamingTool({
							toolCallId: event.data.toolCallId,
							assistantEntryId: event.data.assistantEntryId || existing?.assistantEntryId,
							toolName: event.data.toolName || existing?.toolName || 'tool',
							label: event.data.toolLabel || existing?.label,
							args: existing?.args,
							result: existing?.result,
							rawResult: existing?.rawResult,
							runtimeRoute: existing?.runtimeRoute,
							workerRoutingHint: existing?.workerRoutingHint,
							blockedReason: existing?.blockedReason,
							policySources: existing?.policySources,
							processEvents,
							latestProcessEvent: processEvent,
							processLogRef: existing?.processLogRef,
							status: existing?.status || 'running',
						});
					}
				}
				break;
			case 'tool_call':
				upsertStreamingTool({
					toolCallId: fallbackToolCallId(event),
					toolName: event.toolName,
					label: event.toolLabel,
					args: event.args,
					status: 'running',
				});
				break;
			case 'tool_result': {
				const toolCallId = fallbackToolCallId(event);
				const existing = streamingTools.value.find(item => item.toolCallId === toolCallId)
					|| [...streamingTools.value].reverse().find(item => item.toolName === event.toolName && item.status === 'running');
				upsertStreamingTool({
					toolCallId: existing?.toolCallId || toolCallId,
					toolName: event.toolName,
					label: event.toolLabel,
					result: event.result,
					rawResult: normalizeToolRawResult(event.rawResult),
					status: 'done',
				});
				break;
			}
			case 'clarify_request':
				if (event.data?.requestId) {
					pendingClarify.value = {
						requestId: event.data.requestId,
						question: event.data.question || '',
						choices: Array.isArray(event.data.choices) ? event.data.choices : [],
					};
				}
				break;
			case 'tool_confirmation_request' as WSServerEvent['type']:
				if (event.data?.requestId) {
					pendingToolConfirm.value = {
						requestId: event.data.requestId,
						toolName: event.data.toolName || '',
						toolLabel: event.data.toolLabel || '',
						args: event.data.args || {},
						riskReason: event.data.riskReason || '',
					};
				}
				break;
			case 'error':
				streamingError.value = event.message || '执行失败';
				loading.value = false;
				pendingClarify.value = null;
				pendingToolConfirm.value = null;
				void refreshCurrentSnapshot();
				break;
			case 'done':
				if (event.sessionId) {
					sessionId.value = event.sessionId;
					setStoredSessionId(event.sessionId);
				}
				loading.value = false;
				pendingClarify.value = null;
				pendingToolConfirm.value = null;
				streamingAssistantText.value = '';
				streamingAssistantThinking.value = '';
				streamingError.value = '';
				void loadSessions();
				void refreshCurrentSnapshot();
				break;
			default:
				break;
		}
	}

	async function init() {
		if (initialized.value) return;
		initialized.value = true;
		initWS();
		await loadAgents();
		const storedAgentId = getStoredAgentId();
		if (storedAgentId && agents.value.find(item => item.id === storedAgentId)) {
			currentAgentId.value = storedAgentId;
		}
		await loadSessions();
		const storedSessionId = getStoredSessionId();
		const restoreLastSession = await shouldRestoreLastSession(currentAgentId.value);
		if (storedSessionId && restoreLastSession) {
			try {
				await loadSnapshot(storedSessionId);
				await loadSessions();
			} catch {
				if (sessions.value.find(item => item.sessionId === storedSessionId)) {
					await loadSnapshot(storedSessionId);
				}
			}
		} else if (!restoreLastSession) {
			setStoredSessionId('');
		}
	}

	return {
		initialized,
		loading,
		agents,
		sessions,
		currentAgentId,
		currentAgent,
		sessionId,
		sessionMeta,
		entries,
		entryById,
		childrenByParentId,
		activePathIds,
		subagentRuntimeByBatchId,
		toolRuntimeRoutesByBatchId,
		subagentEvidenceSummariesByEntryId,
		subagentProcessEventsByEntryId,
		subagentTaskPanelsByEntryId,
		subagentProjectionDiagnosticsByEntryId,
		activePathEntries,
		visibleConversationEntries,
		treeRoots,
		selectedEntryId,
		selectedEntry,
		composerPrefill,
		switchingLeafEntryId,
		pendingLeafConfirmation,
		pendingUserContent,
		pendingClarify,
		pendingToolConfirm,
		streamingAssistantEntryId,
		streamingAssistantText,
		streamingAssistantThinking,
		streamingTools,
		streamingError,
		canSend,
		init,
		loadAgents,
		loadSessions,
		loadSnapshot,
		selectAgent,
		newSession,
		switchSession,
		deleteSession,
		deleteAllSessions,
		selectTreeEntry,
		continueFromEntry,
		sendMessage,
		sendClarifyResponse,
		sendToolConfirmResponse,
	};

	function recordSubagentStarted(event: WSServerEvent) {
		const batchId = event.data?.batchId;
		if (!batchId) return;
		const key = String(batchId);
		if (hasSubagentTreeState(key)) return;
		const previous = subagentRuntimeByBatchId.value[key];
		const previousRoutes = toolRuntimeRoutesByBatchId.value[key] || [];
		toolRuntimeRoutesByBatchId.value = {
			...toolRuntimeRoutesByBatchId.value,
			[key]: Array.isArray(event.data?.toolRuntimeRoutes)
				? event.data.toolRuntimeRoutes
				: previousRoutes,
		};
		subagentRuntimeByBatchId.value = {
			...subagentRuntimeByBatchId.value,
			[key]: {
				batchId: key,
				status: 'running',
				mode: event.data.mode === 'parallel' ? 'parallel' : 'single',
				tasks: Array.isArray(event.data.tasks) ? event.data.tasks : previous?.tasks || [],
				results: previous?.results || [],
				events: [
					...(previous?.events || []),
					{ timestamp: Date.now(), kind: 'started', payload: sanitizeRecord(event.data) },
				],
			},
		};
	}

	function recordSubagentEvent(event: WSServerEvent) {
		const batchId = event.data?.batchId;
		if (!batchId) return;
		const key = String(batchId);
		if (hasStoredSubagentProgress(key)) return;
		const previous = subagentRuntimeByBatchId.value[key];
		subagentRuntimeByBatchId.value = {
			...subagentRuntimeByBatchId.value,
			[key]: {
				batchId: key,
				status: previous?.status || 'running',
				mode: previous?.mode,
				tasks: previous?.tasks || [],
				results: previous?.results || [],
				events: [
					...(previous?.events || []),
					{
						timestamp: normalizeSubagentRuntimeTimestamp(event.data?.timestamp),
						sequence: typeof event.data?.sequence === 'number' ? event.data.sequence : undefined,
						kind: 'event',
						status: normalizeSubagentRuntimeStatus(event.data?.event?.status),
						payload: sanitizeRecord(event.data?.event),
					},
				],
			},
		};
	}

	function recordSubagentCompleted(event: WSServerEvent) {
		const batchId = event.data?.batchId;
		if (!batchId) return;
		const key = String(batchId);
		if (hasStoredSubagentProgress(key)) return;
		const previous = subagentRuntimeByBatchId.value[key];
		subagentRuntimeByBatchId.value = {
			...subagentRuntimeByBatchId.value,
			[key]: {
				batchId: key,
				status: 'completed',
				mode: previous?.mode,
				tasks: previous?.tasks || [],
				results: Array.isArray(event.data?.results) ? event.data.results : previous?.results || [],
				events: [
					...(previous?.events || []),
					{ timestamp: Date.now(), kind: 'completed', payload: sanitizeRecord(event.data) },
				],
			},
		};
	}

	function hasSubagentTreeState(batchId: string) {
		return entries.value.some(entry => {
			if (entry.type === 'subagent_batch') {
				return subagentBatchId(entry) === batchId;
			}
			if (entry.type === 'subagent_result') {
				return subagentResultBatchId(entry) === batchId;
			}
			return false;
		});
	}

	function hasStoredSubagentProgress(batchId: string) {
		return entries.value.some(entry => {
			if (entry.type === 'subagent_batch' && subagentBatchId(entry) === batchId) {
				const metadata = getNormalizedSubagentBatchMetadata(entry);
				return subagentBatchContent(entry).status === 'completed'
					|| metadata.events.length > 0
					|| metadata.finalResults.length > 0;
			}

			if (entry.type === 'subagent_result' && subagentResultBatchId(entry) === batchId) {
				return true;
			}

			return false;
		});
	}
});

function subagentBatchId(entry: SessionTreeEntry): string {
	const content = (entry.content || {}) as Partial<SessionTreeSubagentBatchContent>;
	return typeof content.batchId === 'string' ? content.batchId : '';
}

function subagentResultBatchId(entry: SessionTreeEntry): string {
	const content = (entry.content || {}) as Partial<SessionTreeSubagentResultContent>;
	return typeof content.batchId === 'string' ? content.batchId : '';
}

function subagentBatchContent(entry: SessionTreeEntry): Partial<SessionTreeSubagentBatchContent> {
	return (entry.content || {}) as Partial<SessionTreeSubagentBatchContent>;
}

export function mergeSubagentRuntimeWithCanonical(
	previous: Record<string, SubagentRuntimeState>,
	canonical: Record<string, SubagentRuntimeState>,
): Record<string, SubagentRuntimeState> {
	const next: Record<string, SubagentRuntimeState> = {};

	for (const [batchId, state] of Object.entries(previous)) {
		if (!canonical[batchId]) {
			next[batchId] = state;
		}
	}

	for (const [batchId, canonicalState] of Object.entries(canonical)) {
		const previousState = previous[batchId];
		// Session tree entries are the stable source of truth. Keep transient websocket
		// projection only until the persisted batch metadata/result entry catches up.
		const hasCanonicalProgress = canonicalState.status === 'completed'
			|| canonicalState.events.length > 0
			|| canonicalState.results.length > 0;

		next[batchId] = {
			...canonicalState,
			status: hasCanonicalProgress ? canonicalState.status : previousState?.status || canonicalState.status,
			mode: canonicalState.mode || previousState?.mode,
			tasks: canonicalState.tasks.length > 0 ? canonicalState.tasks : previousState?.tasks || [],
			results: canonicalState.results.length > 0 ? canonicalState.results : previousState?.results || [],
			events: canonicalState.events.length > 0 ? canonicalState.events : previousState?.events || [],
		};
	}

	return next;
}

function sanitizeRecord(value: unknown): Record<string, unknown> {
	if (!value || typeof value !== 'object' || Array.isArray(value)) {
		return { value };
	}

	return value as Record<string, unknown>;
}

function normalizeSubagentRuntimeTimestamp(value: unknown): number {
	if (typeof value === 'string') {
		const parsed = Date.parse(value);
		if (!Number.isNaN(parsed)) {
			return parsed;
		}
	}
	if (typeof value === 'number' && Number.isFinite(value)) {
		return value;
	}
	return Date.now();
}

function normalizeSubagentRuntimeStatus(value: unknown): SubagentRuntimeState['events'][number]['status'] {
	return value === 'in_flight' || value === 'completed' || value === 'failed'
		? value
		: undefined;
}

function normalizeToolRawResult(value: unknown): StreamingToolProjection['rawResult'] | undefined {
	if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
	const record = value as Record<string, any>;
	if (record.type === 'text') {
		return { type: 'text', text: typeof record.text === 'string' ? record.text : '' };
	}
	if (record.type === 'json') {
		return { type: 'json', data: record.data };
	}
	if (record.type === 'image') {
		return {
			type: 'image',
			url: typeof record.url === 'string' ? record.url : '',
			text: typeof record.text === 'string' ? record.text : undefined,
			mimeType: typeof record.mimeType === 'string' ? record.mimeType : undefined,
			width: typeof record.width === 'number' ? record.width : undefined,
			height: typeof record.height === 'number' ? record.height : undefined,
			bytes: typeof record.bytes === 'number' ? record.bytes : undefined,
			originalWidth: typeof record.originalWidth === 'number' ? record.originalWidth : undefined,
			originalHeight: typeof record.originalHeight === 'number' ? record.originalHeight : undefined,
			originalBytes: typeof record.originalBytes === 'number' ? record.originalBytes : undefined,
			resized: typeof record.resized === 'boolean' ? record.resized : undefined,
		};
	}
	if (record.type === 'images' && Array.isArray(record.images)) {
		return {
			type: 'images',
			text: typeof record.text === 'string' ? record.text : undefined,
			images: record.images.map((img: any) => ({
				url: typeof img.url === 'string' ? img.url : '',
				mimeType: typeof img.mimeType === 'string' ? img.mimeType : undefined,
				label: typeof img.label === 'string' ? img.label : undefined,
				purpose: typeof img.purpose === 'string' ? img.purpose : undefined,
				reason: typeof img.reason === 'string' ? img.reason : undefined,
				damageId: typeof img.damageId === 'string' ? img.damageId : undefined,
				timestamp: typeof img.timestamp === 'number' ? img.timestamp : undefined,
				width: typeof img.width === 'number' ? img.width : undefined,
				height: typeof img.height === 'number' ? img.height : undefined,
				seed: typeof img.seed === 'number' ? img.seed : undefined,
			})),
		};
	}
	return undefined;
}

function normalizeToolProcessEvents(value: unknown): RuntimeProcessEventProjection[] | undefined {
	if (!Array.isArray(value)) return undefined;
	const events = value
		.map(normalizeToolProcessEvent)
		.filter((item): item is RuntimeProcessEventProjection => Boolean(item));
	return events.length > 0 ? events.slice(-50) : undefined;
}

function normalizeToolProcessEvent(value: unknown): RuntimeProcessEventProjection | undefined {
	if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
	const record = value as Record<string, any>;
	if (typeof record.message !== 'string') return undefined;
	return {
		version: 1,
		operationId: typeof record.operationId === 'string' ? record.operationId : `legacy-${record.toolCallId || record.sequence || Date.now()}`,
		parentOperationId: typeof record.parentOperationId === 'string' ? record.parentOperationId : undefined,
		targetType: ['tool', 'skill', 'subagent', 'task', 'runtime'].includes(record.targetType) ? record.targetType : 'tool',
		source: ['skill', 'tool', 'subagent', 'runtime'].includes(record.source) ? record.source : 'tool',
		taskId: typeof record.taskId === 'string' ? record.taskId : undefined,
		stage: typeof record.stage === 'string' ? record.stage : undefined,
		status: ['queued', 'started', 'running', 'completed', 'failed', 'cancelled', 'skipped'].includes(record.status) ? record.status : 'running',
		level: ['debug', 'info', 'warning', 'error'].includes(record.level) ? record.level : undefined,
		message: record.message,
		detail: typeof record.detail === 'string' ? record.detail : undefined,
		current: typeof record.current === 'number' ? record.current : undefined,
		total: typeof record.total === 'number' ? record.total : undefined,
		percent: typeof record.percent === 'number' ? record.percent : undefined,
		payload: record.payload && typeof record.payload === 'object' && !Array.isArray(record.payload) ? record.payload : undefined,
		timestamp: typeof record.timestamp === 'string' ? record.timestamp : new Date().toISOString(),
		sequence: typeof record.sequence === 'number' ? record.sequence : 0,
	};
}

function normalizeProcessLogRef(value: unknown): RuntimeProcessLogRef | undefined {
	if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
	const record = value as Record<string, any>;
	if (record.kind !== 'runtime_process_log') return undefined;
	if (typeof record.sessionId !== 'string' || typeof record.operationId !== 'string') return undefined;
	return {
		kind: 'runtime_process_log',
		sessionId: record.sessionId,
		operationId: record.operationId,
		toolCallId: typeof record.toolCallId === 'string' ? record.toolCallId : undefined,
	};
}

function hasNonEmptyArray(value: unknown): boolean {
	return Array.isArray(value) && value.length > 0;
}
