import {
	bindExternalStoreMessage,
	type ExportedMessageRepository,
	type ThreadMessage,
} from "@assistant-ui/core";
import { ThreadListPrimitive } from "@assistant-ui/react";
import type { UIMessage } from "ai";
import {
	Loader2Icon,
	MoreHorizontalIcon,
	PencilIcon,
	PlusIcon,
	ServerIcon,
	Trash2Icon,
	XIcon,
} from "lucide-react";
import {
	type FC,
	type KeyboardEvent,
	useEffect,
	useRef,
	useState,
} from "react";
import { Button } from "@/components/ui/button";
import {
	clearActiveSessionId,
	markFreshLocalId,
	readActiveSessionId,
	writeActiveSessionId,
	writePendingWorkspaceSessionId,
} from "@/lib/claw-active-session";
import {
	fetchSessionReplaySnapshot,
	invalidateCachedSessionReplay,
	readCachedSessionReplay,
	type SessionReplaySnapshot,
} from "@/lib/claw-session-cache";
import { useClawSessionReplay } from "@/lib/claw-session-replay";
import { pushHomeUrl, pushSessionUrl } from "@/lib/claw-session-url";

type ClawSession = {
	session_id: string;
	turns: number;
	tool_calls: number;
	preview: string;
	modified_at: number;
};

type ClawStoredMessage = {
	role?: string;
	content?: string;
	message_id?: string;
	name?: string;
	tool_call_id?: string;
	tool_calls?: ClawStoredToolCall[];
	state?: string;
	stop_reason?: string;
	metadata?: {
		elapsed_ms?: unknown;
		kind?: unknown;
		placeholder?: unknown;
		phase?: unknown;
		status?: unknown;
	};
};

type ClawStoredSession = {
	session_id: string;
	messages?: ClawStoredMessage[];
};

export type ClawRunStatus = {
	run_id?: string;
	session_id?: string;
	status?:
		| "idle"
		| "queued"
		| "running"
		| "completed"
		| "failed"
		| "cancelled"
		| "interrupted";
	current_stage?: string;
	started_at?: number;
	updated_at?: number;
	finished_at?: number;
	elapsed_ms?: number;
	cancellable?: boolean;
	pending_prompt?: string;
	events?: ClawRunEvent[];
};

type ClawActiveRunStatus = ClawRunStatus & {
	status: "queued" | "running";
};

type ClawRunEvent = {
	type?: string;
	started_at?: unknown;
	recorded_at?: unknown;
	elapsed_ms?: unknown;
	tool_name?: string;
	tool_call_id?: string;
	arguments?: unknown;
	assistant_content?: string;
	delta?: string;
	ok?: boolean;
	metadata?: {
		output_preview?: unknown;
	};
};

type ClawStoredToolCall = {
	id?: string;
	name?: string;
	arguments?: unknown;
	function?: {
		name?: string;
		arguments?: unknown;
	};
};

type ReplaySessionFn = (
	sessionId: string,
	repository: ExportedMessageRepository,
) => void;

type ClawReplaySnapshot = SessionReplaySnapshot<
	ClawStoredSession,
	ClawRunStatus
>;

const SESSION_BADGE_POLL_INTERVAL_MS = 2000;
const MAX_SESSION_BADGE_POLL_COUNT = 80;

function replaySessionSnapshot(
	replaySession: ReplaySessionFn,
	sessionId: string,
	snapshot: ClawReplaySnapshot,
) {
	replaySession(
		sessionId,
		toReplayRepository(snapshot.session, sessionId, snapshot.runStatus),
	);
}

export const ThreadList: FC = () => {
	return (
		<ThreadListPrimitive.Root className="aui-root aui-thread-list-root flex min-h-0 flex-1 flex-col gap-1">
			<ThreadListNew />
			<JupyterWorkspaceList />
			<ClawSessionList />
		</ThreadListPrimitive.Root>
	);
};

const ThreadListNew: FC = () => {
	const { clearSession } = useClawSessionReplay();
	return (
		<Button
			type="button"
			variant="outline"
			className="aui-thread-list-new h-9 w-full justify-start gap-2 rounded-lg px-3 text-sm hover:bg-muted data-active:bg-muted"
			onClick={() => {
				pushHomeUrl();
				clearActiveSessionId();
				clearSession();
				window.dispatchEvent(new Event("claw-active-session-cleared"));
			}}
		>
			<PlusIcon className="size-4" />
			New Task
		</Button>
	);
};

type JupyterWorkspaceEntry = {
	id: string;
	label?: string;
	base_url?: string;
	workspace_root?: string;
	created_at?: number;
	last_used_at?: number;
};

function ensureSidebarWorkspaceSessionId(): {
	sessionId: string;
	created: boolean;
} {
	const existing = readActiveSessionId();
	if (existing) return { sessionId: existing, created: false };
	const generated =
		typeof crypto !== "undefined" && "randomUUID" in crypto
			? `__LOCALID_${crypto.randomUUID().replace(/-/g, "").slice(0, 8)}`
			: `__LOCALID_${Math.random().toString(36).slice(2, 10)}`;
	markFreshLocalId(generated);
	writeActiveSessionId(generated);
	return { sessionId: generated, created: true };
}

function workspaceDisplayLabel(entry: JupyterWorkspaceEntry): string {
	if (entry.label?.trim()) return entry.label.trim();
	if (entry.base_url) {
		try {
			return new URL(entry.base_url).host;
		} catch {
			return entry.base_url;
		}
	}
	return entry.id;
}

const JupyterWorkspaceList: FC = () => {
	const [entries, setEntries] = useState<JupyterWorkspaceEntry[]>([]);
	const [loaded, setLoaded] = useState(false);
	const [bindingId, setBindingId] = useState<string | null>(null);
	const [error, setError] = useState<string | null>(null);

	useEffect(() => {
		let cancelled = false;
		const refresh = () => {
			fetch("/api/claw/jupyter/workspaces", { cache: "no-store" })
				.then((res) => (res.ok ? res.json() : []))
				.then((payload) => {
					if (cancelled) return;
					if (Array.isArray(payload)) {
						setEntries(payload as JupyterWorkspaceEntry[]);
					} else {
						setEntries([]);
					}
					setLoaded(true);
				})
				.catch(() => {
					if (cancelled) return;
					setEntries([]);
					setLoaded(true);
				});
		};
		refresh();
		const onChanged = () => refresh();
		window.addEventListener("claw-workspaces-changed", onChanged);
		window.addEventListener("focus", refresh);
		return () => {
			cancelled = true;
			window.removeEventListener("claw-workspaces-changed", onChanged);
			window.removeEventListener("focus", refresh);
		};
	}, []);

	if (!loaded || entries.length === 0) return null;

	const bind = async (entry: JupyterWorkspaceEntry) => {
		setError(null);
		setBindingId(entry.id);
		const { sessionId, created } = ensureSidebarWorkspaceSessionId();
		try {
			const response = await fetch(
				`/api/claw/jupyter/workspaces/${encodeURIComponent(entry.id)}/bind`,
				{
					method: "POST",
					headers: { "content-type": "application/json" },
					body: JSON.stringify({ session_id: sessionId }),
				},
			);
			if (!response.ok) {
				const payload = (await response.json().catch(() => ({}))) as {
					detail?: string;
					error?: string;
				};
				setError(payload.detail ?? payload.error ?? "切换工作区失败");
				return;
			}
			if (created) writePendingWorkspaceSessionId(sessionId);
			window.dispatchEvent(new Event("claw-sessions-changed"));
			window.dispatchEvent(new Event("claw-workspaces-changed"));
		} catch (err) {
			setError(err instanceof Error ? err.message : "切换工作区时网络异常");
		} finally {
			setBindingId(null);
		}
	};

	const remove = async (entry: JupyterWorkspaceEntry) => {
		if (!window.confirm(`移除工作区「${workspaceDisplayLabel(entry)}」？`))
			return;
		try {
			const response = await fetch(
				`/api/claw/jupyter/workspaces/${encodeURIComponent(entry.id)}`,
				{ method: "DELETE" },
			);
			if (!response.ok) return;
			setEntries((current) => current.filter((e) => e.id !== entry.id));
		} catch {
			// best effort; next refresh will reconcile
		}
	};

	return (
		<div className="mt-1 flex flex-col gap-1 border-y border-dashed py-2">
			<div className="flex items-center gap-1.5 px-2 text-muted-foreground text-xs uppercase tracking-wide">
				<ServerIcon className="size-3" />
				<span>工作区</span>
			</div>
			<div className="flex flex-col gap-0.5">
				{entries.map((entry) => {
					const label = workspaceDisplayLabel(entry);
					const subtitle = entry.workspace_root ?? "";
					const isBinding = bindingId === entry.id;
					return (
						<div
							key={entry.id}
							className="group relative flex h-9 items-center gap-1 rounded-lg transition-colors hover:bg-muted"
						>
							<button
								type="button"
								disabled={isBinding}
								onClick={() => void bind(entry)}
								className="flex h-full min-w-0 flex-1 items-center gap-2 rounded-lg px-3 text-left text-sm disabled:cursor-wait"
								title={`${entry.base_url ?? ""}${subtitle ? ` · ${subtitle}` : ""}`}
							>
								{isBinding ? (
									<Loader2Icon className="size-3.5 shrink-0 animate-spin" />
								) : (
									<ServerIcon className="size-3.5 shrink-0 text-muted-foreground" />
								)}
								<span className="min-w-0 flex-1 truncate">{label}</span>
							</button>
							<button
								type="button"
								aria-label="移除工作区"
								title="移除工作区"
								onClick={() => void remove(entry)}
								className="mr-1 hidden size-6 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-background hover:text-foreground group-hover:flex"
							>
								<XIcon className="size-3.5" />
							</button>
						</div>
					);
				})}
			</div>
			{error ? (
				<div className="px-2 text-destructive text-xs">{error}</div>
			) : null}
		</div>
	);
};

const ClawSessionList: FC = () => {
	const { replaySession } = useClawSessionReplay();
	const [sessions, setSessions] = useState<ClawSession[]>([]);
	const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
	const [loadingSessionId, setLoadingSessionId] = useState<string | null>(null);
	const [deletingSessionId, setDeletingSessionId] = useState<string | null>(
		null,
	);
	const [renamingSessionId, setRenamingSessionId] = useState<string | null>(
		null,
	);
	const [draftTitle, setDraftTitle] = useState("");
	const [openMenuSessionId, setOpenMenuSessionId] = useState<string | null>(
		null,
	);
	const renameInputRef = useRef<HTMLInputElement | null>(null);
	const openRequestRef = useRef(0);
	const openAbortRef = useRef<AbortController | null>(null);
	const sessionBadges = useSessionRunBadges(sessions, activeSessionId);

	useEffect(() => {
		setActiveSessionId(readActiveSessionId());
		const refreshSessions = () => {
			fetch("/api/claw/sessions")
				.then((res) => (res.ok ? res.json() : []))
				.then((payload) => {
					if (Array.isArray(payload)) setSessions(dedupeSessions(payload));
				})
				.catch(() => setSessions([]));
		};
		const clearActiveSession = () => {
			setActiveSessionId(null);
			refreshSessions();
		};
		window.addEventListener("claw-active-session-cleared", clearActiveSession);
		window.addEventListener("claw-sessions-changed", refreshSessions);
		window.addEventListener("focus", refreshSessions);
		refreshSessions();
		return () => {
			window.removeEventListener(
				"claw-active-session-cleared",
				clearActiveSession,
			);
			window.removeEventListener("claw-sessions-changed", refreshSessions);
			window.removeEventListener("focus", refreshSessions);
		};
	}, []);

	useEffect(() => {
		if (!renamingSessionId) return;
		window.requestAnimationFrame(() => {
			renameInputRef.current?.focus();
			renameInputRef.current?.select();
		});
	}, [renamingSessionId]);

	if (!sessions.length) return null;

	const saveTitle = async (sessionId: string, fallbackTitle: string) => {
		const normalized = draftTitle.trim();
		if (!normalized || normalized === fallbackTitle) {
			setRenamingSessionId(null);
			setDraftTitle("");
			return;
		}
		const response = await fetch(`/api/claw/sessions/${sessionId}`, {
			method: "PATCH",
			headers: { "content-type": "application/json" },
			body: JSON.stringify({ title: normalized }),
		});
		if (!response.ok) return;
		const payload = (await response.json()) as { title?: string };
		const nextTitle = payload.title || normalized;
		setSessions((current) =>
			current.map((item) =>
				item.session_id === sessionId ? { ...item, preview: nextTitle } : item,
			),
		);
		setRenamingSessionId(null);
		setDraftTitle("");
	};

	const openSession = async (sessionId: string) => {
		sessionBadges.markRead(sessionId);
		const requestId = openRequestRef.current + 1;
		openRequestRef.current = requestId;
		openAbortRef.current?.abort();
		const abortController = new AbortController();
		openAbortRef.current = abortController;
		const cached = readCachedSessionReplay<ClawStoredSession, ClawRunStatus>(
			sessionId,
		);

		writeActiveSessionId(sessionId);
		pushSessionUrl(sessionId);
		setActiveSessionId(sessionId);
		if (cached) {
			replaySessionSnapshot(replaySession, sessionId, cached);
			setLoadingSessionId(null);
		} else {
			setLoadingSessionId(sessionId);
		}

		try {
			const snapshot = await fetchSessionReplaySnapshot<
				ClawStoredSession,
				ClawRunStatus
			>(sessionId, { signal: abortController.signal });
			if (requestId !== openRequestRef.current) return;
			if (readActiveSessionId() !== sessionId) return;
			if (!snapshot) return;
			if (!cached || snapshot.signature !== cached.signature) {
				replaySessionSnapshot(replaySession, sessionId, snapshot);
			}
		} catch (error) {
			if (!(error instanceof DOMException && error.name === "AbortError")) {
				console.warn("[session-list] failed to open session", error);
			}
		} finally {
			if (requestId === openRequestRef.current) {
				setLoadingSessionId(null);
			}
			if (openAbortRef.current === abortController) {
				openAbortRef.current = null;
			}
		}
	};

	return (
		<div className="min-h-0 flex-1 overflow-y-auto pr-1">
			<div className="flex flex-col gap-1">
				{sessions.map((session) => {
					const active = activeSessionId === session.session_id;
					const loading = loadingSessionId === session.session_id;
					const badge = sessionBadges.bySessionId.get(session.session_id);
					return (
						<div
							key={`claw-session-${session.session_id}-${session.modified_at}`}
							className="aui-thread-list-item group relative flex h-9 items-center rounded-lg transition-colors hover:bg-muted data-[active=true]:bg-muted"
							data-active={active}
						>
							{renamingSessionId === session.session_id ? (
								<div className="aui-thread-list-item-trigger flex h-full min-w-0 flex-1 items-center px-3">
									<input
										ref={renameInputRef}
										type="text"
										className="h-7 min-w-0 flex-1 rounded-md border bg-background px-2 text-sm outline-none focus-visible:ring-1 focus-visible:ring-ring"
										value={draftTitle}
										onChange={(event) => setDraftTitle(event.target.value)}
										onBlur={() => {
											saveTitle(
												session.session_id,
												session.preview || session.session_id,
											);
										}}
										onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => {
											if (event.key === "Enter") {
												event.preventDefault();
												event.currentTarget.blur();
											}
											if (event.key === "Escape") {
												event.preventDefault();
												setRenamingSessionId(null);
												setDraftTitle("");
											}
										}}
									/>
								</div>
							) : (
								<button
									type="button"
									className="aui-thread-list-item-trigger flex h-full min-w-0 flex-1 items-center overflow-hidden px-3 pr-1 text-start text-sm"
									onClick={async () => {
										console.log("[session-list] click", {
											sessionId: session.session_id,
											preview: session.preview,
										});
										setOpenMenuSessionId(null);
										await openSession(session.session_id);
									}}
								>
									<span className="min-w-0 truncate">
										{loading
											? "回放中..."
											: session.preview || session.session_id}
									</span>
								</button>
							)}
							<SessionActionSlot
								running={badge?.running === true}
								unread={badge?.unread === true}
								busy={deletingSessionId === session.session_id}
								open={openMenuSessionId === session.session_id}
								onOpenChange={() => {
									setOpenMenuSessionId((current) =>
										current === session.session_id ? null : session.session_id,
									);
								}}
							/>
							{openMenuSessionId === session.session_id ? (
								<div className="aui-thread-list-item-more-content absolute top-8 right-1 z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md">
									<button
										type="button"
										className="aui-thread-list-item-more-item flex w-full cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm outline-none hover:bg-accent focus:bg-accent"
										disabled={renamingSessionId === session.session_id}
										onClick={() => {
											setRenamingSessionId(session.session_id);
											setDraftTitle(session.preview || session.session_id);
											setOpenMenuSessionId(null);
										}}
									>
										<PencilIcon className="size-4" />
										重命名
									</button>
									<button
										type="button"
										className="aui-thread-list-item-more-item flex w-full cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-left text-sm text-destructive outline-none hover:bg-accent focus:bg-accent"
										disabled={deletingSessionId === session.session_id}
										onClick={async () => {
											setDeletingSessionId(session.session_id);
											try {
												const response = await fetch(
													`/api/claw/sessions/${session.session_id}`,
													{ method: "DELETE" },
												);
												if (!response.ok) return;
												setSessions((current) =>
													current.filter(
														(item) => item.session_id !== session.session_id,
													),
												);
												invalidateCachedSessionReplay(session.session_id);
												setOpenMenuSessionId(null);
												if (activeSessionId === session.session_id) {
													clearActiveSessionId();
													setActiveSessionId(null);
													window.dispatchEvent(
														new Event("claw-active-session-cleared"),
													);
												}
											} finally {
												setDeletingSessionId(null);
											}
										}}
									>
										<Trash2Icon className="size-4" />
										删除
									</button>
								</div>
							) : null}
						</div>
					);
				})}
			</div>
		</div>
	);
};

function useSessionRunBadges(
	sessions: ClawSession[],
	activeSessionId: string | null,
) {
	const [runningIds, setRunningIds] = useState<Set<string>>(() => new Set());
	const [unreadIds, setUnreadIds] = useState<Set<string>>(() => new Set());
	const previousRunningIdsRef = useRef<Set<string>>(new Set());

	useEffect(() => {
		if (!activeSessionId) return;
		setUnreadIds((current) => {
			if (!current.has(activeSessionId)) return current;
			const next = new Set(current);
			next.delete(activeSessionId);
			return next;
		});
	}, [activeSessionId]);

	useEffect(() => {
		const polledSessionIds = sessions
			.slice(0, MAX_SESSION_BADGE_POLL_COUNT)
			.map((session) => session.session_id);
		const knownSessionIds = new Set(polledSessionIds);
		if (polledSessionIds.length === 0) {
			setRunningIds(new Set());
			setUnreadIds(new Set());
			previousRunningIdsRef.current = new Set();
			return;
		}
		let cancelled = false;

		async function refreshBadges() {
			const results = await Promise.allSettled(
				polledSessionIds.map(async (sessionId) => ({
					sessionId,
					status: await fetchLatestRunStatus(sessionId),
				})),
			);
			if (cancelled) return;

			const nextRunningIds = new Set<string>();
			const completedSessionIds: string[] = [];
			for (const result of results) {
				if (result.status !== "fulfilled") continue;
				const { sessionId, status } = result.value;
				if (isActiveRunStatus(status)) {
					nextRunningIds.add(sessionId);
				} else if (previousRunningIdsRef.current.has(sessionId)) {
					completedSessionIds.push(sessionId);
				}
			}

			setRunningIds(nextRunningIds);
			setUnreadIds((current) => {
				const next = new Set(
					[...current].filter((sessionId) => knownSessionIds.has(sessionId)),
				);
				for (const sessionId of completedSessionIds) {
					if (sessionId !== activeSessionId) {
						next.add(sessionId);
					}
				}
				if (activeSessionId) next.delete(activeSessionId);
				return next;
			});
			previousRunningIdsRef.current = nextRunningIds;
		}

		void refreshBadges();
		const interval = window.setInterval(
			refreshBadges,
			SESSION_BADGE_POLL_INTERVAL_MS,
		);
		window.addEventListener("claw-sessions-changed", refreshBadges);
		return () => {
			cancelled = true;
			window.clearInterval(interval);
			window.removeEventListener("claw-sessions-changed", refreshBadges);
		};
	}, [sessions, activeSessionId]);

	const bySessionId = new Map<string, { running: boolean; unread: boolean }>();
	for (const session of sessions) {
		bySessionId.set(session.session_id, {
			running: runningIds.has(session.session_id),
			unread: unreadIds.has(session.session_id),
		});
	}

	const markRead = (sessionId: string) => {
		setUnreadIds((current) => {
			if (!current.has(sessionId)) return current;
			const next = new Set(current);
			next.delete(sessionId);
			return next;
		});
	};

	return { bySessionId, markRead };
}

function SessionActionSlot({
	running,
	unread,
	busy,
	open,
	onOpenChange,
}: {
	running: boolean;
	unread: boolean;
	busy: boolean;
	open: boolean;
	onOpenChange: () => void;
}) {
	const hasBadge = running || unread;
	return (
		<div className="relative mr-2 flex size-7 shrink-0 items-center justify-center">
			<span
				aria-hidden={!hasBadge}
				className="pointer-events-none absolute inset-0 flex items-center justify-center text-muted-foreground transition-opacity group-hover:opacity-0 data-[hidden=true]:opacity-0 data-[open=true]:opacity-0"
				data-hidden={!hasBadge}
				data-open={open || busy}
				title={running ? "运行中" : unread ? "已完成，未查看" : undefined}
			>
				{running ? (
					<Loader2Icon className="size-3.5 animate-spin" />
				) : unread ? (
					<span className="size-2 rounded-full bg-sky-300" />
				) : null}
			</span>
			<button
				type="button"
				className="aui-thread-list-item-more absolute inset-0 flex items-center justify-center rounded-md p-0 text-muted-foreground opacity-0 transition-opacity hover:bg-accent hover:text-foreground group-hover:opacity-100 data-[busy=true]:opacity-100 data-[open=true]:bg-accent data-[open=true]:opacity-100"
				data-busy={busy}
				data-open={open}
				aria-expanded={open}
				aria-label="打开历史会话菜单"
				title="更多选项"
				onClick={onOpenChange}
			>
				<MoreHorizontalIcon className="size-4" />
			</button>
		</div>
	);
}

function dedupeSessions(payload: unknown[]) {
	const byId = new Map<string, ClawSession>();
	for (const item of payload) {
		if (!isClawSession(item)) continue;
		const previous = byId.get(item.session_id);
		if (!previous || item.modified_at >= previous.modified_at) {
			byId.set(item.session_id, item);
		}
	}
	return [...byId.values()].sort((a, b) => b.modified_at - a.modified_at);
}

export async function fetchLatestRunStatus(sessionId: string) {
	try {
		const url = new URL("/api/claw/runs/latest", window.location.origin);
		url.searchParams.set("session_id", sessionId);
		const response = await fetch(url, { cache: "no-store" });
		if (!response.ok) return null;
		return (await response.json()) as ClawRunStatus;
	} catch {
		return null;
	}
}

function isClawSession(value: unknown): value is ClawSession {
	if (!value || typeof value !== "object") return false;
	const item = value as Partial<ClawSession>;
	return (
		typeof item.session_id === "string" && typeof item.modified_at === "number"
	);
}

export function toReplayRepository(
	session: ClawStoredSession,
	fallbackSessionId: string,
	runStatus?: ClawRunStatus | null,
): ExportedMessageRepository {
	const messages: ExportedMessageRepository["messages"] = [];
	const replayMessages = getReplayMessages(session.messages ?? [], runStatus);
	const toolResults = collectToolResults(replayMessages);
	const lastAssistantContentIndex =
		findLastAssistantContentIndex(replayMessages);
	let previousId: string | null = null;
	for (const [index, message] of replayMessages.entries()) {
		if (message.role === "tool") continue;
		if (message.role !== "user" && message.role !== "assistant") continue;
		const content = cleanStoredContent(message.content ?? "");
		const toolParts =
			message.role === "assistant"
				? toToolParts(message.tool_calls ?? [], toolResults, content)
				: [];
		if (!content && !toolParts.length) continue;
		if (content.trimStart().startsWith("<system-reminder>")) continue;
		const id = replayMessageId(fallbackSessionId, message, index);
		const shouldShowAssistantText =
			message.role !== "assistant" ||
			!toolParts.length ||
			index === lastAssistantContentIndex;
		const elapsedMs =
			message.role === "assistant"
				? resolveReplayElapsedMs(replayMessages, index)
				: undefined;
		const parts = (
			message.role === "assistant" && toolParts.length
				? shouldShowAssistantText
					? [...toolParts, { type: "text", text: content }]
					: toolParts
				: message.role === "assistant" && elapsedMs !== undefined
					? [
							{ type: "reasoning", text: "思考并执行完成。", elapsedMs },
							{ type: "text", text: content },
						]
					: [{ type: "text", text: content }]
		) as UIMessage["parts"];
		const replayParts = withReplayElapsedMs(parts, elapsedMs);
		const uiMessage: UIMessage = {
			id,
			role: message.role,
			parts: replayParts,
			...(message.role === "assistant"
				? {
						metadata: {
							sessionId: session.session_id ?? fallbackSessionId,
							elapsedMs,
						},
					}
				: {}),
		} as UIMessage;
		const threadMessage = {
			id,
			role: message.role,
			createdAt: new Date(),
			content: toThreadMessageContent(replayParts, elapsedMs),
			...(message.role === "assistant"
				? {
						status: { type: "complete", reason: "stop" },
						metadata: {
							unstable_state: null,
							unstable_annotations: [],
							unstable_data: [],
							steps: [],
							custom: {
								sessionId: session.session_id ?? fallbackSessionId,
								elapsedMs,
							},
						},
					}
				: { metadata: { custom: {} } }),
		} as ThreadMessage;
		bindExternalStoreMessage(threadMessage, uiMessage);
		messages.push({ message: threadMessage, parentId: previousId });
		previousId = id;
	}
	if (shouldShowTerminalPendingPrompt(replayMessages, runStatus)) {
		const pendingPrompt = resolvePendingPrompt(replayMessages, runStatus);
		if (pendingPrompt) {
			const id = `${fallbackSessionId}-pending-${runStatus?.run_id ?? "terminal"}`;
			const parts = [
				{ type: "text", text: pendingPrompt },
			] as UIMessage["parts"];
			const uiMessage: UIMessage = {
				id,
				role: "user",
				parts,
				metadata: { sessionId: session.session_id ?? fallbackSessionId },
			} as UIMessage;
			const threadMessage = {
				id,
				role: "user",
				createdAt: new Date(),
				content: toThreadMessageContent(parts),
				attachments: [],
				metadata: {
					custom: { sessionId: session.session_id ?? fallbackSessionId },
				},
			} as ThreadMessage;
			bindExternalStoreMessage(threadMessage, uiMessage);
			messages.push({ message: threadMessage, parentId: previousId });
			previousId = id;
		}
	}
	if (shouldShowInterruptedNotice(session.messages ?? [], runStatus)) {
		const id = `${fallbackSessionId}-interrupted`;
		const text =
			runStatus?.status === "cancelled"
				? "上一次任务已取消，后台没有正在执行的进程。你可以继续回复，或重新发起任务。"
				: "上一次任务已中断，后台没有正在执行的进程。你可以继续回复，或重新发起任务。";
		const parts = [{ type: "text", text }] as UIMessage["parts"];
		const uiMessage: UIMessage = {
			id,
			role: "assistant",
			parts,
			metadata: { sessionId: session.session_id ?? fallbackSessionId },
		} as UIMessage;
		const threadMessage = {
			id,
			role: "assistant",
			createdAt: new Date(),
			content: toThreadMessageContent(parts),
			status: { type: "complete", reason: "stop" },
			metadata: {
				unstable_state: null,
				unstable_annotations: [],
				unstable_data: [],
				steps: [],
				custom: { sessionId: session.session_id ?? fallbackSessionId },
			},
		} as ThreadMessage;
		bindExternalStoreMessage(threadMessage, uiMessage);
		messages.push({ message: threadMessage, parentId: previousId });
		previousId = id;
	}
	if (isActiveRunStatus(runStatus)) {
		const pendingPrompt = resolvePendingPrompt(replayMessages, runStatus);
		if (pendingPrompt) {
			const id = `${fallbackSessionId}-pending-${runStatus.run_id ?? "active"}`;
			const parts = [
				{ type: "text", text: pendingPrompt },
			] as UIMessage["parts"];
			const uiMessage: UIMessage = {
				id,
				role: "user",
				parts,
				metadata: { sessionId: session.session_id ?? fallbackSessionId },
			} as UIMessage;
			const threadMessage = {
				id,
				role: "user",
				createdAt: new Date(),
				content: toThreadMessageContent(parts),
				attachments: [],
				metadata: {
					custom: { sessionId: session.session_id ?? fallbackSessionId },
				},
			} as ThreadMessage;
			bindExternalStoreMessage(threadMessage, uiMessage);
			messages.push({ message: threadMessage, parentId: previousId });
			previousId = id;
		}
		const id = `${fallbackSessionId}-run-${runStatus.run_id ?? "active"}`;
		const runStartedAtMs = resolveRunStartedAtMs(runStatus);
		const runCreatedAt = new Date(runStartedAtMs ?? Date.now());
		const parts = buildActiveRunParts(runStatus) as UIMessage["parts"];
		const uiMessage: UIMessage = {
			id,
			role: "assistant",
			parts,
			metadata: {
				sessionId: session.session_id ?? fallbackSessionId,
				runId: runStatus.run_id,
				runStartedAtMs,
				custom: {
					sessionId: session.session_id ?? fallbackSessionId,
					runId: runStatus.run_id,
					runStartedAtMs,
				},
			},
		} as UIMessage;
		const threadMessage = {
			id,
			role: "assistant",
			createdAt: runCreatedAt,
			content: toThreadMessageContent(parts),
			status: { type: "running" },
			metadata: {
				unstable_state: null,
				unstable_annotations: [],
				unstable_data: [],
				steps: [],
				sessionId: session.session_id ?? fallbackSessionId,
				runId: runStatus.run_id,
				runStartedAtMs,
				custom: {
					sessionId: session.session_id ?? fallbackSessionId,
					runId: runStatus.run_id,
					runStartedAtMs,
				},
			},
		} as ThreadMessage;
		bindExternalStoreMessage(threadMessage, uiMessage);
		messages.push({ message: threadMessage, parentId: previousId });
		previousId = id;
	}
	return {
		headId: previousId,
		messages,
	};
}

function isActiveRunStatus(
	runStatus?: ClawRunStatus | null,
): runStatus is ClawActiveRunStatus {
	return runStatus?.status === "queued" || runStatus?.status === "running";
}

function shouldShowTerminalPendingPrompt(
	messages: readonly ClawStoredMessage[],
	runStatus?: ClawRunStatus | null,
) {
	return (
		!isActiveRunStatus(runStatus) &&
		Boolean(resolvePendingPrompt(messages, runStatus)) &&
		(runStatus?.status === "interrupted" ||
			runStatus?.status === "cancelled" ||
			runStatus?.status === "failed")
	);
}

function buildActiveRunParts(runStatus: ClawActiveRunStatus) {
	const eventLines = summarizeRunEvents(runStatus);
	const fallback =
		runStatus.status === "queued"
			? "当前会话已有任务在执行，本轮正在排队。"
			: runStatus.current_stage || "后端正在执行这个会话。";
	const reasoningText = eventLines.length ? eventLines.join("\n") : fallback;
	const runStartedAtMs = resolveRunStartedAtMs(runStatus);
	const elapsedMs = normalizeElapsedMs(runStatus.elapsed_ms);
	return [
		{ type: "reasoning", text: reasoningText, runStartedAtMs, elapsedMs },
		...buildRunToolParts(runStatus.events ?? []),
	];
}

function summarizeRunEvents(runStatus: ClawActiveRunStatus) {
	const lines: string[] = [];
	for (const event of runStatus.events ?? []) {
		if (event.type === "run_queued") {
			lines.push("当前会话已有任务在执行，本轮正在排队。");
		}
		if (event.type === "run_started") {
			lines.push("后端已开始执行本轮任务。");
		}
		if (event.type === "tool_start") {
			const stageNote =
				typeof event.assistant_content === "string"
					? event.assistant_content.trim()
					: "";
			lines.push(
				stageNote ||
					(event.tool_name ? `调用工具 ${event.tool_name}` : "正在调用工具"),
			);
		}
		if (event.type === "tool_result") {
			lines.push(
				event.tool_name ? `工具完成 ${event.tool_name}` : "工具调用完成",
			);
		}
		if (event.type === "tool_delta") {
			lines.push(event.tool_name ? `${event.tool_name} 输出中` : "工具输出中");
		}
		if (event.type === "final_text_start") {
			lines.push("正在整理回复");
		}
		if (event.type === "user_review_required") {
			lines.push("等待用户 review");
		}
	}
	if (runStatus.current_stage && lines.at(-1) !== runStatus.current_stage) {
		lines.push(runStatus.current_stage);
	}
	return lines.slice(-40);
}

function buildRunToolParts(events: readonly ClawRunEvent[]) {
	const resultById = new Map<string, ClawRunEvent>();
	for (const event of events) {
		if (event.type === "tool_result" && event.tool_call_id) {
			resultById.set(event.tool_call_id, event);
		}
	}
	const seen = new Set<string>();
	return events.flatMap((event) => {
		if (
			event.type !== "tool_start" ||
			!event.tool_call_id ||
			!event.tool_name ||
			seen.has(event.tool_call_id)
		) {
			return [];
		}
		seen.add(event.tool_call_id);
		const result = resultById.get(event.tool_call_id);
		const outputPreview = result?.metadata?.output_preview;
		const output =
			typeof outputPreview === "string"
				? outputPreview
				: result
					? result.ok === false
						? "工具执行失败，等待最终结果汇总。"
						: "工具调用完成，等待最终结果汇总。"
					: undefined;
		return [
			{
				type: "dynamic-tool",
				toolName: event.tool_name,
				toolCallId: event.tool_call_id,
				state: output === undefined ? "input-available" : "output-available",
				input: attachStageNote(event.arguments ?? {}, event.assistant_content),
				...(output === undefined ? {} : { output }),
			},
		];
	});
}

function getReplayMessages(
	messages: readonly ClawStoredMessage[],
	runStatus?: ClawRunStatus | null,
) {
	const visibleMessages = isActiveRunStatus(runStatus)
		? messages.filter((message) => !isRunStatusMessage(message))
		: [...messages];
	return trimIncompleteReplayTail(visibleMessages);
}

function shouldShowInterruptedNotice(
	messages: readonly ClawStoredMessage[],
	runStatus?: ClawRunStatus | null,
) {
	if (isActiveRunStatus(runStatus)) return false;
	if (hasRunStatusMessage(messages, runStatus?.status)) return false;
	return (
		runStatus?.status === "interrupted" ||
		runStatus?.status === "cancelled" ||
		trimIncompleteReplayTail(messages).length !== messages.length
	);
}

function trimIncompleteReplayTail(messages: readonly ClawStoredMessage[]) {
	const trimmed = [...messages];
	while (trimmed.length && isIncompleteReplayMessage(trimmed.at(-1))) {
		trimmed.pop();
	}
	while (trimmed.length && hasTrailingAssistantToolCall(trimmed.at(-1))) {
		trimmed.pop();
	}
	return trimmed;
}

function isIncompleteReplayMessage(message?: ClawStoredMessage) {
	if (!message) return false;
	if (message.state && message.state !== "final") return true;
	if (
		message.role === "tool" &&
		(message.metadata?.phase === "starting" ||
			message.metadata?.phase === "running")
	) {
		return true;
	}
	if (
		message.role !== "user" &&
		message.metadata?.placeholder === true &&
		message.metadata?.status === "running"
	) {
		return true;
	}
	return false;
}

function hasTrailingAssistantToolCall(message?: ClawStoredMessage) {
	return message?.role === "assistant" && Boolean(message.tool_calls?.length);
}

function hasRunStatusMessage(
	messages: readonly ClawStoredMessage[],
	status?: string,
) {
	return messages.some(
		(message) =>
			isRunStatusMessage(message) &&
			(!status || message.metadata?.status === status),
	);
}

function isRunStatusMessage(message?: ClawStoredMessage) {
	return message?.metadata?.kind === "run_status";
}

function replayMessageId(
	sessionId: string,
	message: ClawStoredMessage,
	index: number,
) {
	const stableId =
		typeof message.message_id === "string" && message.message_id.trim()
			? message.message_id.trim()
			: String(index);
	return `${sessionId}-${stableId}`;
}

function resolvePendingPrompt(
	messages: readonly ClawStoredMessage[],
	runStatus?: ClawRunStatus | null,
) {
	const pendingPrompt =
		typeof runStatus?.pending_prompt === "string"
			? runStatus.pending_prompt.trim()
			: "";
	if (!pendingPrompt) return null;
	for (const message of messages) {
		if (!message || message.role !== "user") continue;
		const content = cleanStoredContent(message.content ?? "").trim();
		if (!content || content.trimStart().startsWith("<system-reminder>"))
			continue;
		if (content === pendingPrompt) return null;
	}
	return pendingPrompt;
}

function normalizeRunTimestampMs(value: unknown) {
	if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
	return value < 10_000_000_000 ? Math.round(value * 1000) : Math.round(value);
}

function normalizeElapsedMs(value: unknown) {
	if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
	return Math.max(0, Math.round(value));
}

function resolveRunStartedAtMs(runStatus: ClawRunStatus) {
	const direct = normalizeRunTimestampMs(runStatus.started_at);
	if (direct !== undefined) return direct;

	const updatedAt = normalizeRunTimestampMs(runStatus.updated_at);
	const elapsedMs = normalizeElapsedMs(runStatus.elapsed_ms);
	if (updatedAt !== undefined && elapsedMs !== undefined) {
		return Math.max(0, updatedAt - elapsedMs);
	}

	for (const event of runStatus.events ?? []) {
		if (event.type !== "run_started") continue;
		const eventStarted = normalizeRunTimestampMs(event.started_at);
		if (eventStarted !== undefined) return eventStarted;
		const recordedAt = normalizeRunTimestampMs(event.recorded_at);
		if (recordedAt !== undefined) return recordedAt;
	}

	for (const event of runStatus.events ?? []) {
		const recordedAt = normalizeRunTimestampMs(event.recorded_at);
		const elapsedMs = normalizeElapsedMs(event.elapsed_ms);
		if (recordedAt !== undefined && elapsedMs !== undefined) {
			return Math.max(0, recordedAt - elapsedMs);
		}
	}

	for (const event of runStatus.events ?? []) {
		const recordedAt = normalizeRunTimestampMs(event.recorded_at);
		if (recordedAt !== undefined) return recordedAt;
	}

	return undefined;
}

function resolveReplayElapsedMs(
	messages: readonly ClawStoredMessage[],
	index: number,
) {
	const current = normalizeElapsedMs(messages[index]?.metadata?.elapsed_ms);
	if (current !== undefined) return current;
	for (let i = index + 1; i < messages.length; i += 1) {
		const message = messages[i];
		if (message?.role === "user") return undefined;
		if (message?.role !== "assistant") continue;
		const elapsedMs = normalizeElapsedMs(message.metadata?.elapsed_ms);
		if (elapsedMs !== undefined) return elapsedMs;
	}
	return undefined;
}

function findLastAssistantContentIndex(messages: readonly ClawStoredMessage[]) {
	for (let index = messages.length - 1; index >= 0; index -= 1) {
		const message = messages[index];
		if (message?.role !== "assistant") continue;
		const content = cleanStoredContent(message.content ?? "");
		if (!content) continue;
		return index;
	}
	return -1;
}

function cleanStoredContent(content: string) {
	const markers = ["\n\n[当前会话目录]", "\n[当前会话目录]"];
	for (const marker of markers) {
		if (content.includes(marker)) return content.split(marker, 1)[0].trim();
	}
	return content.trim();
}

function collectToolResults(messages: readonly ClawStoredMessage[]) {
	const results = new Map<string, string>();
	for (const message of messages) {
		if (
			message.role === "tool" &&
			message.tool_call_id &&
			!isIncompleteReplayMessage(message)
		) {
			results.set(message.tool_call_id, message.content ?? "");
		}
	}
	return results;
}

function toToolParts(
	toolCalls: readonly ClawStoredToolCall[],
	toolResults: Map<string, string>,
	stageNote?: string,
) {
	return toolCalls.flatMap((call) => {
		const toolCallId = call.id;
		const toolName = call.function?.name ?? call.name;
		if (!toolCallId || !toolName) return [];
		const input = attachStageNote(
			parseToolInput(call.function?.arguments ?? call.arguments),
			stageNote,
		);
		const output = toolResults.get(toolCallId);
		return [
			{
				type: "dynamic-tool",
				toolName,
				toolCallId,
				state: output === undefined ? "input-available" : "output-available",
				input,
				...(output === undefined ? {} : { output }),
			},
		];
	});
}

function attachStageNote(input: unknown, stageNote?: string) {
	const note = stageNote?.trim();
	if (!note) return input;
	if (input && typeof input === "object" && !Array.isArray(input)) {
		return { ...input, __claw_stage_note: note };
	}
	return { value: input, __claw_stage_note: note };
}

function withReplayElapsedMs(parts: UIMessage["parts"], elapsedMs?: number) {
	if (elapsedMs === undefined) return parts;
	return parts.map((part) => {
		if (part.type === "reasoning") return { ...part, elapsedMs };
		if (part.type === "dynamic-tool") {
			return {
				...part,
				elapsedMs,
				input: attachReplayElapsed(part.input ?? {}, elapsedMs),
			};
		}
		return part;
	}) as UIMessage["parts"];
}

function attachReplayElapsed(input: unknown, elapsedMs: number) {
	if (input && typeof input === "object" && !Array.isArray(input)) {
		return { ...input, __claw_elapsed_ms: elapsedMs };
	}
	return { value: input, __claw_elapsed_ms: elapsedMs };
}

function toThreadMessageContent(parts: UIMessage["parts"], elapsedMs?: number) {
	return parts.map((part) => {
		const elapsedPayload = elapsedMs === undefined ? {} : { elapsedMs };
		if (part.type === "text") return part;
		if (part.type === "reasoning") return { ...part, ...elapsedPayload };
		if (part.type === "dynamic-tool") {
			return {
				type: "tool-call",
				toolName: part.toolName,
				toolCallId: part.toolCallId,
				args: part.input ?? {},
				argsText: JSON.stringify(stripReplayElapsed(part.input ?? {})),
				...elapsedPayload,
				...(part.state === "output-available" ? { result: part.output } : {}),
			};
		}
		return part;
	});
}

function stripReplayElapsed(value: unknown): unknown {
	if (!value || typeof value !== "object" || Array.isArray(value)) return value;
	const { __claw_elapsed_ms: _elapsedMs, ...rest } = value as Record<
		string,
		unknown
	>;
	return rest;
}

function parseToolInput(value: unknown) {
	if (typeof value !== "string") return value ?? {};
	try {
		return JSON.parse(value);
	} catch {
		return value;
	}
}
