"use client";

import type {
	MessageState,
	ThreadAssistantMessagePart,
	ToolCallMessagePart,
	ToolCallMessagePartStatus,
} from "@assistant-ui/react";
import { useAuiState } from "@assistant-ui/react";
import {
	BrainIcon,
	CheckCircle2Icon,
	ChevronDownIcon,
	ClockIcon,
	CloudUploadIcon,
	DownloadIcon,
	FileTextIcon,
	Loader2Icon,
	MessageSquareTextIcon,
	PanelRightCloseIcon,
	WrenchIcon,
	XCircleIcon,
} from "lucide-react";
import {
	createContext,
	type ReactNode,
	useCallback,
	useContext,
	useEffect,
	useMemo,
	useRef,
	useState,
} from "react";
import {
	type ClawRunStatus,
	fetchLatestRunStatus,
} from "@/components/assistant-ui/thread-list";
import { Button } from "@/components/ui/button";
import {
	Collapsible,
	CollapsibleContent,
	CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
	ACTIVE_SESSION_CHANGED_EVENT,
	readActiveSessionId,
	writeActiveSessionId,
} from "@/lib/claw-active-session";
import { cn } from "@/lib/utils";

type ActivityContextValue = {
	open: boolean;
	mode: "activity" | "files";
	selectedId: string | null;
	sessionId: string | null;
	filesRefreshToken: number;
	openActivity: () => void;
	openItem: (id: string) => void;
	openFiles: (sessionId?: string | null) => void;
	close: () => void;
};

const ActivityContext = createContext<ActivityContextValue | null>(null);

export function ActivityProvider({ children }: { children: ReactNode }) {
	const [open, setOpen] = useState(false);
	const [mode, setMode] = useState<"activity" | "files">("activity");
	const [selectedId, setSelectedId] = useState<string | null>(null);
	const [sessionId, setSessionId] = useState<string | null>(null);
	const [filesRefreshToken, setFilesRefreshToken] = useState(0);
	const openActivity = useCallback(() => {
		setMode("activity");
		setSelectedId(null);
		setOpen(true);
	}, []);
	const openItem = useCallback((id: string) => {
		setMode("activity");
		setSelectedId(id);
		setOpen(true);
	}, []);
	const openFiles = useCallback((nextSessionId?: string | null) => {
		setMode("files");
		setSessionId(nextSessionId ?? readActiveSessionId());
		setFilesRefreshToken((value) => value + 1);
		setOpen(true);
	}, []);
	const close = useCallback(() => setOpen(false), []);
	const value = useMemo(
		() => ({
			open,
			mode,
			selectedId,
			sessionId,
			filesRefreshToken,
			openActivity,
			openItem,
			openFiles,
			close,
		}),
		[
			open,
			mode,
			selectedId,
			sessionId,
			filesRefreshToken,
			openActivity,
			openItem,
			openFiles,
			close,
		],
	);

	return (
		<ActivityContext.Provider value={value}>
			{children}
		</ActivityContext.Provider>
	);
}

export function useActivityPanel() {
	const value = useContext(ActivityContext);
	if (!value) {
		throw new Error("useActivityPanel must be used within ActivityProvider");
	}
	return value;
}

type ActivityItem = {
	id: string;
	kind: "reasoning" | "tool";
	title: string;
	summary: string;
	status: ToolCallMessagePartStatus["type"];
	argsText?: string;
	result?: unknown;
	resultSummary?: string;
	rawResult?: string;
	fileLinks?: string[];
};

type LiveRunEvent = NonNullable<ClawRunStatus["events"]>[number];

export function ActivityPanel() {
	const {
		open,
		mode,
		selectedId,
		sessionId,
		filesRefreshToken,
		openActivity,
		openItem,
		close,
	} = useActivityPanel();
	const messages = useAuiState((s) => s.thread.messages);
	const [activeSessionId, setActiveSessionId] = useState<string | null>(() =>
		readActiveSessionId(),
	);
	const liveRunStatus = useLiveRunStatus(activeSessionId, mode);
	const replayedSessionId = useMemo(
		() => findLatestSessionIdInMessages(messages),
		[messages],
	);
	const messageItems = useMemo(
		() => collectActivityItems(messages),
		[messages],
	);
	const liveItems = useMemo(
		() => collectLiveRunItems(liveRunStatus),
		[liveRunStatus],
	);
	const selectedMessageId = selectedId ? activityMessageId(selectedId) : null;
	const selectedMessageItems = selectedMessageId
		? messageItems.filter(
				(item) => activityMessageId(item.id) === selectedMessageId,
			)
		: [];
	const shouldUseLiveItems =
		!selectedMessageId &&
		liveItems.length > 0 &&
		activeSessionId !== null &&
		(activeSessionId !== replayedSessionId || hasActiveRun(liveRunStatus));
	const items = shouldUseLiveItems ? liveItems : messageItems;
	const selectedItems = selectedMessageId
		? items.filter((item) => activityMessageId(item.id) === selectedMessageId)
		: [];
	const visibleItems =
		selectedMessageId && selectedMessageItems.length > 0
			? selectedMessageItems
			: selectedMessageId && selectedItems.length > 0
				? selectedItems
				: items;
	const selectedActivityMissing = Boolean(
		selectedMessageId &&
			!shouldUseLiveItems &&
			messageItems.length > 0 &&
			selectedMessageItems.length === 0 &&
			selectedItems.length === 0,
	);
	const prevLatestIdRef = useRef<string | null>(null);

	useEffect(() => {
		const update = () => setActiveSessionId(readActiveSessionId());
		const handleChanged = (event: Event) => {
			const detail = (event as CustomEvent<{ sessionId?: string | null }>)
				.detail;
			setActiveSessionId(detail?.sessionId ?? readActiveSessionId());
		};
		window.addEventListener(ACTIVE_SESSION_CHANGED_EVENT, handleChanged);
		window.addEventListener("focus", update);
		update();
		return () => {
			window.removeEventListener(ACTIVE_SESSION_CHANGED_EVENT, handleChanged);
			window.removeEventListener("focus", update);
		};
	}, []);

	useEffect(() => {
		const latestId = items.at(-1)?.id ?? null;
		if (
			mode === "activity" &&
			!selectedId &&
			latestId &&
			latestId !== prevLatestIdRef.current
		) {
			openActivity();
		}
		prevLatestIdRef.current = latestId;
	}, [items, mode, openActivity, selectedId]);

	if (!open) return null;

	if (mode === "files") {
		return (
			<SessionFilesPanel
				key={`${sessionId ?? "active"}:${filesRefreshToken}`}
				sessionId={sessionId}
				onClose={close}
			/>
		);
	}

	return (
		<aside className="fixed inset-y-0 right-0 z-30 flex w-[min(23rem,calc(100vw-1rem))] flex-col border-l bg-background shadow-xl lg:static lg:z-auto lg:h-full lg:w-92 lg:shrink-0 lg:shadow-none">
			<header className="flex h-16 shrink-0 items-center justify-between border-b px-4">
				<div className="min-w-0">
					<h2 className="font-semibold text-base">活动</h2>
					<p className="text-muted-foreground text-xs">
						{visibleItems.length > 0
							? `${visibleItems.length} 个步骤`
							: "暂无链路"}
					</p>
				</div>
				<Button
					type="button"
					variant="ghost"
					size="icon"
					className="size-8"
					onClick={close}
					aria-label="关闭活动面板"
				>
					<PanelRightCloseIcon className="size-4" />
				</Button>
			</header>
			<div className="min-h-0 flex-1 overflow-y-auto px-4 py-4">
				{visibleItems.length === 0 ? (
					<p className="text-muted-foreground text-sm">
						当前这轮对话还没有工具链路。
					</p>
				) : (
					<div className="flex flex-col gap-2">
						{selectedActivityMissing ? (
							<p className="rounded-md bg-muted/50 px-3 py-2 text-muted-foreground text-xs">
								刷新后未能精确定位到这条摘要，已显示当前会话可恢复的活动链路。
							</p>
						) : null}
						{visibleItems.map((item) => (
							<ActivityPanelItem
								key={item.id}
								item={item}
								open={selectedId === item.id}
								onOpenChange={(nextOpen) => {
									if (nextOpen) openItem(item.id);
								}}
							/>
						))}
					</div>
				)}
			</div>
		</aside>
	);
}

type SessionFile = {
	name: string;
	path: string;
	kind: "input" | "output";
	source?: "local" | "jupyter";
	size: number;
	modified_at: string;
	download_url: string;
	online_doc_url?: string | null;
	online_doc_title?: string | null;
	online_doc_kind?: string | null;
	online_doc_updated_at?: number | null;
};

type SessionFilesPayload = {
	session_id?: string | null;
	input?: SessionFile[];
	output?: SessionFile[];
	error?: string;
};

function SessionFilesPanel({
	sessionId,
	onClose,
}: {
	sessionId: string | null;
	onClose: () => void;
}) {
	const [payload, setPayload] = useState<SessionFilesPayload | null>(null);
	const [status, setStatus] = useState("正在读取聊天中的文件...");

	useEffect(() => {
		let cancelled = false;
		async function loadFiles() {
			const activeSessionId = normalizeSessionId(
				sessionId ?? readActiveSessionId(),
			);
			setStatus("正在读取聊天中的文件...");
			try {
				let { response, payload: nextPayload } =
					await fetchSessionFiles(activeSessionId);
				if (
					activeSessionId &&
					response.ok &&
					sessionFilesCount(nextPayload) === 0
				) {
					const latest = await fetchSessionFiles(null);
					if (
						latest.response.ok &&
						latest.payload.session_id &&
						latest.payload.session_id !== activeSessionId
					) {
						response = latest.response;
						nextPayload = latest.payload;
					}
				}
				if (cancelled) return;
				if (!response.ok) {
					setPayload({ input: [], output: [], error: nextPayload.error });
					setStatus(nextPayload.error ?? "读取文件失败");
					return;
				}
				if (nextPayload.session_id) {
					writeActiveSessionId(nextPayload.session_id);
				}
				setPayload(nextPayload);
				setStatus("");
			} catch (err) {
				if (!cancelled) {
					setPayload({ input: [], output: [] });
					setStatus(err instanceof Error ? err.message : "读取文件失败");
				}
			}
		}
		loadFiles();
		const handleRefresh = () => {
			loadFiles();
		};
		window.addEventListener("claw-sessions-changed", handleRefresh);
		window.addEventListener("claw-session-files-changed", handleRefresh);
		window.addEventListener("focus", handleRefresh);
		return () => {
			cancelled = true;
			window.removeEventListener("claw-sessions-changed", handleRefresh);
			window.removeEventListener("claw-session-files-changed", handleRefresh);
			window.removeEventListener("focus", handleRefresh);
		};
	}, [sessionId]);

	const inputFiles = payload?.input ?? [];
	const outputFiles = payload?.output ?? [];
	const total = inputFiles.length + outputFiles.length;

	return (
		<aside className="fixed inset-y-0 right-0 z-30 flex w-[min(25rem,calc(100vw-1rem))] flex-col border-l bg-background shadow-xl lg:static lg:z-auto lg:h-full lg:w-96 lg:shrink-0 lg:shadow-none">
			<header className="flex h-16 shrink-0 items-center justify-between border-b px-4">
				<div className="min-w-0">
					<h2 className="font-semibold text-base">聊天中的文件</h2>
					<p className="text-muted-foreground text-xs">
						{status || `${total} 个文件`}
					</p>
				</div>
				<Button
					type="button"
					variant="ghost"
					size="icon"
					className="size-8"
					onClick={onClose}
					aria-label="关闭文件面板"
				>
					<PanelRightCloseIcon className="size-4" />
				</Button>
			</header>
			<div className="min-h-0 flex-1 overflow-y-auto px-4 py-4">
				{!status && total === 0 ? (
					<p className="text-muted-foreground text-sm">
						当前聊天还没有上传文件或输出文件。
					</p>
				) : null}
				<FileSection title="输入" files={inputFiles} />
				<FileSection title="输出" files={outputFiles} />
			</div>
		</aside>
	);
}

async function fetchSessionFiles(sessionId: string | null) {
	const url = new URL("/api/claw/files", window.location.origin);
	if (sessionId) url.searchParams.set("session_id", sessionId);
	const response = await fetch(url, { cache: "no-store" });
	const payload = (await response.json()) as SessionFilesPayload;
	return { response, payload };
}

function sessionFilesCount(payload: SessionFilesPayload) {
	return (payload.input?.length ?? 0) + (payload.output?.length ?? 0);
}

function normalizeSessionId(value: unknown) {
	if (typeof value !== "string") return null;
	const trimmed = value.trim();
	return trimmed || null;
}

function FileSection({
	title,
	files,
}: {
	title: string;
	files: SessionFile[];
}) {
	return (
		<section className="mb-5">
			<div className="mb-2 flex items-center justify-between">
				<h3 className="font-medium text-sm">{title}</h3>
				<span className="text-muted-foreground text-xs">{files.length}</span>
			</div>
			{files.length ? (
				<div className="flex flex-col gap-2">
					{files.map((file) => (
						<SessionFileRow key={`${file.kind}:${file.path}`} file={file} />
					))}
				</div>
			) : (
				<p className="rounded-lg border border-dashed px-3 py-4 text-center text-muted-foreground text-xs">
					暂无{title}文件
				</p>
			)}
		</section>
	);
}

function SessionFileRow({ file }: { file: SessionFile }) {
	const [onlineDoc, setOnlineDoc] = useState<{
		status: "idle" | "loading" | "auth" | "done" | "error";
		message?: string;
		url?: string | null;
		kind?: string | null;
	}>({
		status: file.online_doc_url ? "done" : "idle",
		message: file.online_doc_url
			? `已转${onlineFileKindLabel(file.online_doc_kind, file.name)}`
			: undefined,
		url: file.online_doc_url ?? null,
		kind: file.online_doc_kind ?? null,
	});
	const canCreateOnlineDoc = isOnlineDocSupported(file.name);
	const onlineDocUrl = onlineDoc.url ?? file.online_doc_url ?? null;

	async function createOnlineDoc() {
		if (!canCreateOnlineDoc || onlineDoc.status === "loading") return;
		let pendingWindow: Window | null = null;
		try {
			pendingWindow = window.open("about:blank", "_blank");
			if (pendingWindow) {
				pendingWindow.document.write("正在创建飞书在线文件...");
			}
		} catch {
			pendingWindow = null;
		}
		setOnlineDoc({ status: "loading", message: "正在转换..." });
		try {
			const response = await fetch("/api/claw/files/online-doc", {
				method: "POST",
				headers: { "content-type": "application/json" },
				body: JSON.stringify({ path: file.path, title: file.name }),
			});
			const payload = (await response.json().catch(() => ({}))) as Record<
				string,
				unknown
			>;
			if (response.status === 409) {
				const loginResponse = await fetch("/api/claw/integrations/feishu", {
					method: "POST",
					headers: { "content-type": "application/json" },
					body: JSON.stringify({ action: "login" }),
				});
				const loginPayload = (await loginResponse
					.json()
					.catch(() => ({}))) as FeishuLoginPayload;
				if (!loginResponse.ok) {
					pendingWindow?.close();
					setOnlineDoc({
						status: "error",
						message:
							extractErrorMessage(
								loginPayload as unknown as Record<string, unknown>,
							) ?? "飞书授权启动失败",
					});
					return;
				}
				const loginUrl = loginPayload.login?.login_url ?? null;
				if (loginUrl) {
					if (pendingWindow) {
						try {
							pendingWindow.opener = null;
						} catch {
							// 有些浏览器不允许改 opener，不影响后续跳转。
						}
						pendingWindow.location.href = loginUrl;
					} else {
						window.open(loginUrl, "_blank", "noopener,noreferrer");
					}
				} else if (pendingWindow) {
					pendingWindow.close();
				}
				setOnlineDoc({
					status: "auth",
					message: loginUrl ? "完成授权后再点一次" : "需要先完成飞书授权",
				});
				return;
			}
			if (!response.ok) {
				pendingWindow?.close();
				setOnlineDoc({
					status: "error",
					message: extractErrorMessage(payload) ?? "转换失败",
				});
				return;
			}
			const url = typeof payload.url === "string" ? payload.url : null;
			const kind = typeof payload.kind === "string" ? payload.kind : null;
			if (url) {
				if (pendingWindow) {
					try {
						pendingWindow.opener = null;
					} catch {
						// 有些浏览器不允许改 opener，不影响后续跳转。
					}
					pendingWindow.location.href = url;
				} else {
					window.open(url, "_blank", "noopener,noreferrer");
				}
			} else {
				pendingWindow?.close();
			}
			setOnlineDoc({
				status: "done",
				message: url
					? `已生成${onlineFileKindLabel(kind, file.name)}`
					: "已生成",
				url,
				kind,
			});
			window.dispatchEvent(new CustomEvent("claw-session-files-changed"));
		} catch (error) {
			pendingWindow?.close();
			setOnlineDoc({
				status: "error",
				message: error instanceof Error ? error.message : "转换失败",
			});
		}
	}

	return (
		<div className="flex items-center gap-3 rounded-lg border px-3 py-2">
			<div className="flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground">
				<FileTextIcon className="size-4" />
			</div>
			<div className="min-w-0 flex-1">
				{onlineDocUrl ? (
					<a
						href={onlineDocUrl}
						target="_blank"
						rel="noreferrer"
						className="block truncate font-medium text-primary text-sm underline-offset-4 hover:underline"
						title={`${file.name} · 打开${onlineFileKindLabel(onlineDoc.kind ?? file.online_doc_kind, file.name)}`}
					>
						{file.name}
					</a>
				) : (
					<div className="truncate font-medium text-sm" title={file.name}>
						{file.name}
					</div>
				)}
				<div className="text-muted-foreground text-xs">
					{file.source === "jupyter" ? "JUPYTER · " : ""}
					{fileExtension(file.name).toUpperCase() || "FILE"} ·{" "}
					{formatBytes(file.size)}
				</div>
				{onlineDoc.message ? (
					<div
						className={cn(
							"truncate text-xs",
							onlineDoc.status === "error"
								? "text-destructive"
								: "text-muted-foreground",
						)}
						title={onlineDoc.message}
					>
						{onlineDoc.message}
					</div>
				) : null}
			</div>
			<a
				href={file.download_url}
				className="flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
				aria-label={`下载 ${file.name}`}
				title="下载"
			>
				<DownloadIcon className="size-4" />
			</a>
			<button
				type="button"
				className="flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
				aria-label={`针对 ${file.name} 提问`}
				title="针对文件提问"
				onClick={() => askAboutFile(file)}
			>
				<MessageSquareTextIcon className="size-4" />
			</button>
			{canCreateOnlineDoc ? (
				<button
					type="button"
					className="flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:cursor-not-allowed disabled:opacity-50"
					aria-label={`将 ${file.name} 转为飞书在线文件`}
					title="转为飞书在线文件"
					disabled={onlineDoc.status === "loading"}
					onClick={createOnlineDoc}
				>
					{onlineDoc.status === "loading" ? (
						<Loader2Icon className="size-4 animate-spin" />
					) : (
						<CloudUploadIcon className="size-4" />
					)}
				</button>
			) : null}
		</div>
	);
}

type FeishuLoginPayload = {
	login?: {
		login_url?: string | null;
		user_code?: string | null;
	};
};

function isOnlineDocSupported(fileName: string) {
	return ["csv", "docx", "md", "txt", "xlsx"].includes(
		fileExtension(fileName).toLowerCase(),
	);
}

function onlineFileKindLabel(
	kind: string | null | undefined,
	fileName: string,
) {
	const normalized = kind?.toLowerCase();
	if (normalized === "sheet") return "在线表格";
	if (["csv", "xlsx"].includes(fileExtension(fileName).toLowerCase())) {
		return "在线表格";
	}
	return "在线文档";
}

function extractErrorMessage(payload: Record<string, unknown>) {
	const detail = payload.detail;
	if (typeof detail === "string") return detail;
	if (detail && typeof detail === "object") {
		const message = (detail as { message?: unknown }).message;
		if (typeof message === "string") return message;
	}
	const error = payload.error;
	return typeof error === "string" ? error : null;
}

function askAboutFile(file: SessionFile) {
	window.dispatchEvent(
		new CustomEvent("claw-composer-insert", {
			detail: {
				text: [
					`请针对这个${file.kind === "input" ? "输入" : "输出"}文件回答我的问题：`,
					`- 文件名: ${file.name}`,
					`- 本地路径: ${file.path}`,
					"请先使用 read_file 工具读取文件内容。",
				].join("\n"),
			},
		}),
	);
}

function fileExtension(fileName: string) {
	const ext = fileName.split(".").at(-1);
	return ext && ext !== fileName ? ext : "";
}

function formatBytes(bytes: number) {
	if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
	if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
	return `${bytes} B`;
}

function activityMessageId(activityId: string) {
	return activityId.split(":", 1)[0] ?? activityId;
}

function useLiveRunStatus(
	sessionId: string | null,
	mode: ActivityContextValue["mode"],
) {
	const [status, setStatus] = useState<ClawRunStatus | null>(null);

	useEffect(() => {
		if (!sessionId || mode !== "activity") {
			setStatus(null);
			return;
		}
		const safeSessionId = sessionId;
		let cancelled = false;

		async function refresh() {
			const nextStatus = await fetchLatestRunStatus(safeSessionId);
			if (cancelled) return;
			setStatus(hasActiveRun(nextStatus) ? nextStatus : null);
		}

		refresh();
		const interval = window.setInterval(refresh, 1000);
		return () => {
			cancelled = true;
			window.clearInterval(interval);
		};
	}, [sessionId, mode]);

	return status;
}

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

function findLatestSessionIdInMessages(messages: readonly MessageState[]) {
	for (const message of [...messages].reverse()) {
		const metadata = message.metadata;
		const custom =
			metadata && typeof metadata === "object" && "custom" in metadata
				? (metadata as { custom?: unknown }).custom
				: null;
		if (custom && typeof custom === "object" && "sessionId" in custom) {
			const sessionId = (custom as { sessionId?: unknown }).sessionId;
			if (typeof sessionId === "string" && sessionId.trim()) {
				return sessionId.trim();
			}
		}
	}
	return null;
}

function ActivityPanelItem({
	item,
	open,
	onOpenChange,
}: {
	item: ActivityItem;
	open: boolean;
	onOpenChange: (open: boolean) => void;
}) {
	const Icon = item.kind === "tool" ? WrenchIcon : BrainIcon;

	return (
		<Collapsible
			open={open}
			onOpenChange={onOpenChange}
			className="rounded-lg border bg-card text-card-foreground"
		>
			<CollapsibleTrigger className="flex w-full items-start gap-3 px-3 py-3 text-left">
				<ActivityStatusIcon status={item.status} />
				<div className="min-w-0 flex-1">
					<div className="flex items-center gap-2">
						<Icon className="size-3.5 shrink-0 text-muted-foreground" />
						<span className="truncate font-medium text-sm">{item.title}</span>
					</div>
					<p className="mt-1 line-clamp-2 text-muted-foreground text-xs">
						{item.summary}
					</p>
				</div>
				<ChevronDownIcon
					className={cn(
						"mt-0.5 size-4 shrink-0 text-muted-foreground transition-transform",
						!open && "-rotate-90",
					)}
				/>
			</CollapsibleTrigger>
			<CollapsibleContent>
				<div className="flex flex-col gap-3 border-t px-3 py-3 text-xs">
					{item.argsText ? (
						<ActivityPre title="输入参数" value={item.argsText} />
					) : null}
					{item.resultSummary ? (
						<ActivityResultSummary value={item.resultSummary} />
					) : null}
					{item.fileLinks?.length ? (
						<ActivityFileLinks paths={item.fileLinks} />
					) : null}
					{item.rawResult ? (
						<Collapsible>
							<CollapsibleTrigger className="flex items-center gap-1 font-medium text-muted-foreground transition-colors hover:text-foreground">
								<ChevronDownIcon className="size-3" />
								原始结果
							</CollapsibleTrigger>
							<CollapsibleContent>
								<ActivityPre title="" value={item.rawResult} className="mt-2" />
							</CollapsibleContent>
						</Collapsible>
					) : null}
					{item.kind === "reasoning" ? (
						<p className="text-muted-foreground">
							这里展示的是思考状态摘要，不展示模型完整隐藏思考过程。
						</p>
					) : null}
				</div>
			</CollapsibleContent>
		</Collapsible>
	);
}

function ActivityStatusIcon({ status }: { status: ActivityItem["status"] }) {
	if (status === "running") {
		return <ClockIcon className="mt-0.5 size-4 shrink-0 animate-pulse" />;
	}
	if (status === "incomplete") {
		return <XCircleIcon className="mt-0.5 size-4 shrink-0 text-destructive" />;
	}
	return (
		<CheckCircle2Icon className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
	);
}

function ActivityPre({
	title,
	value,
	className,
}: {
	title: string;
	value: string;
	className?: string;
}) {
	return (
		<div className={className}>
			{title ? (
				<p className="mb-1 font-medium text-muted-foreground">{title}</p>
			) : null}
			<pre className="max-h-80 overflow-auto rounded-md bg-muted px-3 py-2 whitespace-pre-wrap">
				{value}
			</pre>
		</div>
	);
}

function ActivityResultSummary({ value }: { value: string }) {
	return (
		<div className="rounded-md border bg-muted/30 px-3 py-2">
			<div className="mb-1 flex items-center gap-1.5 font-medium text-muted-foreground">
				<FileTextIcon className="size-3.5" />
				结果摘要
			</div>
			<p className="whitespace-pre-wrap">{value}</p>
		</div>
	);
}

function collectLiveRunItems(runStatus: ClawRunStatus | null) {
	if (!hasActiveRun(runStatus)) return [];
	const items: ActivityItem[] = [];
	const events = runStatus.events ?? [];
	const summaryLines = summarizeLiveRunEvents(runStatus, events);
	items.push({
		id: `live:${runStatus.run_id ?? runStatus.session_id ?? "active"}:reasoning`,
		kind: "reasoning",
		title: runStatus.status === "queued" ? "排队中" : "思考中",
		summary: summaryLines.join("\n"),
		status: "running",
	});

	const resultById = new Map<string, LiveRunEvent>();
	for (const event of events) {
		if (event.type === "tool_result" && event.tool_call_id) {
			resultById.set(event.tool_call_id, event);
		}
	}
	const seenToolCalls = new Set<string>();
	for (const event of events) {
		if (
			event.type !== "tool_start" ||
			!event.tool_call_id ||
			!event.tool_name ||
			seenToolCalls.has(event.tool_call_id)
		) {
			continue;
		}
		seenToolCalls.add(event.tool_call_id);
		const result = resultById.get(event.tool_call_id);
		const stageNote =
			typeof event.assistant_content === "string"
				? event.assistant_content.trim()
				: "";
		if (stageNote) {
			items.push({
				id: `live:${runStatus.run_id ?? runStatus.session_id ?? "active"}:${event.tool_call_id}:stage`,
				kind: "reasoning",
				title: "阶段说明",
				summary: stageNote,
				status: result
					? result.ok === false
						? "incomplete"
						: "complete"
					: "running",
			});
		}
		const resultSummary =
			typeof result?.metadata?.output_preview === "string"
				? result.metadata.output_preview
				: result
					? result.ok === false
						? "工具执行失败，等待最终结果汇总。"
						: "工具调用完成，等待最终结果汇总。"
					: undefined;
		const input = stripClawToolMetadata(
			attachStageNote(event.arguments ?? {}, event.assistant_content),
		);
		const argsText = formatToolArgs(input);
		items.push({
			id: `live:${runStatus.run_id ?? runStatus.session_id ?? "active"}:${event.tool_call_id}`,
			kind: "tool",
			title: event.tool_name,
			summary: result
				? result.ok === false
					? "工具调用失败"
					: "工具调用完成"
				: "工具调用中",
			status: result
				? result.ok === false
					? "incomplete"
					: "complete"
				: "running",
			argsText,
			resultSummary,
			rawResult: resultSummary,
			fileLinks: extractLocalPaths(
				[argsText, resultSummary].filter(Boolean) as string[],
			),
		});
	}
	return items;
}

function summarizeLiveRunEvents(
	runStatus: ClawRunStatus,
	events: readonly LiveRunEvent[],
) {
	const lines: string[] = [];
	for (const event of 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 (runStatus.current_stage && lines.at(-1) !== runStatus.current_stage) {
		lines.push(runStatus.current_stage);
	}
	if (!lines.length) {
		lines.push(
			runStatus.status === "queued"
				? "当前会话已有任务在执行，本轮正在排队。"
				: "后端正在执行这个会话。",
		);
	}
	return lines.slice(-40);
}

function isNoisyLiveDelta(value: string) {
	return value === "。" || value === "，" || value === "," || value.length <= 1;
}

function collectActivityItems(messages: readonly MessageState[]) {
	const items: ActivityItem[] = [];

	for (const message of messages) {
		if (message.role !== "assistant") continue;
		const parts = message.content as readonly ThreadAssistantMessagePart[];
		for (const [index, part] of parts.entries()) {
			const id = `${message.id}:${index}`;
			if (part.type === "reasoning") {
				const status = getPartStatus(message, part);
				const summary =
					part.text.trim() ||
					(status === "running"
						? "模型正在整理下一步行动。"
						: "思考并执行完成。");
				items.push({
					id,
					kind: "reasoning",
					title: reasoningTitle(summary, status),
					summary,
					status,
				});
			}
			if (part.type === "tool-call") {
				const input = stripClawToolMetadata(part.args);
				const stageNote = getStageNote(part.args);
				const argsText = formatToolArgs(input);
				if (stageNote) {
					items.push({
						id: `${id}:stage`,
						kind: "reasoning",
						title: "阶段说明",
						summary: stageNote,
						status: getPartStatus(message, part),
					});
				}
				items.push({
					id,
					kind: "tool",
					title: part.toolName,
					summary: summarizeTool(message, part),
					status: getPartStatus(message, part),
					argsText,
					result: decodeJsonString(part.result),
					resultSummary: summarizeToolResult(decodeJsonString(part.result)),
					rawResult:
						part.result === undefined
							? undefined
							: formatValue(decodeJsonString(part.result)),
					fileLinks: extractLocalPaths(
						[
							argsText,
							part.result === undefined
								? undefined
								: formatValue(decodeJsonString(part.result)),
						].filter(Boolean) as string[],
					),
				});
			}
		}
	}

	return items;
}

function getStageNote(value: unknown) {
	if (!value || typeof value !== "object" || Array.isArray(value)) return "";
	const note = (value as Record<string, unknown>).__claw_stage_note;
	return typeof note === "string" ? note.trim() : "";
}

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

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

function formatToolArgs(value: unknown) {
	if (value === undefined) return undefined;
	if (typeof value === "string") return value;
	return JSON.stringify(value ?? {}, null, 2);
}

function reasoningTitle(
	text: string,
	status: ToolCallMessagePartStatus["type"],
) {
	if (status === "running") return "思考中";
	const normalized = text.trim();
	if (
		normalized.startsWith("思考中") ||
		normalized.startsWith("思考并执行完成")
	) {
		return "思考完成";
	}
	return "阶段说明";
}

function ActivityFileLinks({ paths }: { paths: string[] }) {
	return (
		<div className="rounded-md border bg-muted/30 px-3 py-2">
			<div className="mb-1 flex items-center gap-1.5 font-medium text-muted-foreground">
				<FileTextIcon className="size-3.5" />
				相关文件
			</div>
			<div className="flex flex-col gap-1">
				{paths.map((filePath) => (
					<a
						key={filePath}
						href={`/api/claw/files?path=${encodeURIComponent(filePath)}`}
						className="truncate text-primary hover:underline"
						target="_blank"
						rel="noreferrer"
						title={filePath}
					>
						{basename(filePath)}
					</a>
				))}
			</div>
		</div>
	);
}

function getPartStatus(
	message: Extract<MessageState, { role: "assistant" }>,
	part: ThreadAssistantMessagePart,
): ToolCallMessagePartStatus["type"] {
	if (message.status.type === "incomplete") return "incomplete";
	if (part.type === "tool-call" && part.result === undefined) return "running";
	if (message.status.type === "running") return "running";
	return "complete";
}

function summarizeTool(
	message: Extract<MessageState, { role: "assistant" }>,
	part: ToolCallMessagePart,
) {
	const status = getPartStatus(message, part);
	if (status === "running") return "调用工具中";
	if (status === "incomplete") return "工具调用未完成";
	if (part.result === undefined) return "已提交工具参数";
	return "工具调用完成";
}

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

function formatValue(value: unknown) {
	const decoded = decodeJsonString(value);
	return typeof decoded === "string"
		? decoded
		: JSON.stringify(decoded, null, 2);
}

function summarizeToolResult(value: unknown) {
	const decoded = decodeJsonString(value);
	if (decoded === undefined) return undefined;
	if (typeof decoded === "string") return trimText(decoded, 600);
	if (!decoded || typeof decoded !== "object") return String(decoded);

	const record = decoded as Record<string, unknown>;
	const parts: string[] = [];
	if (typeof record.tool === "string") parts.push(`工具: ${record.tool}`);
	if (typeof record.ok === "boolean")
		parts.push(record.ok ? "状态: 成功" : "状态: 失败");
	if (typeof record.error === "string") parts.push(`错误: ${record.error}`);
	if (typeof record.content === "string")
		parts.push(trimText(record.content, 600));
	if (!parts.length) parts.push(trimText(JSON.stringify(record, null, 2), 600));
	return parts.join("\n");
}

function trimText(text: string, maxLength: number) {
	const normalized = text.trim();
	if (normalized.length <= maxLength) return normalized;
	return `${normalized.slice(0, maxLength)}...`;
}

function extractLocalPaths(values: string[]) {
	const paths = new Set<string>();
	for (const value of values) {
		for (const match of value.matchAll(/\/Users\/[^\s"',)]+/g)) {
			paths.add(match[0]);
		}
	}
	return [...paths];
}

function basename(filePath: string) {
	return filePath.split("/").filter(Boolean).at(-1) ?? filePath;
}
