import type { ExportedMessageRepository } from "@assistant-ui/core";
import {
	BarChart3Icon,
	BotIcon,
	BrainIcon,
	ChevronDownIcon,
	Clock3Icon,
	LogOutIcon,
	PanelLeftOpenIcon,
	SaveIcon,
	SearchIcon,
	SquarePenIcon,
	UserIcon,
} from "lucide-react";
import { Popover as PopoverPrimitive } from "radix-ui";
import type * as React from "react";
import { useEffect, useRef, useState } from "react";
import type { ClawAccount } from "@/app/claw-account-gate";
import { ClawLlmSettings } from "@/app/claw-llm-settings";
import { ClawThemeSwitcher } from "@/app/claw-theme-switcher";
import {
	type ClawRunStatus,
	ThreadList,
	toReplayRepository,
} from "@/components/assistant-ui/thread-list";
import { Button } from "@/components/ui/button";
import {
	Dialog,
	DialogContent,
	DialogDescription,
	DialogHeader,
	DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import {
	Sidebar,
	SidebarContent,
	SidebarFooter,
	SidebarHeader,
	SidebarMenu,
	SidebarMenuButton,
	SidebarMenuItem,
	SidebarRail,
	SidebarTrigger,
	useSidebar,
} from "@/components/ui/sidebar";
import {
	clearActiveSessionId,
	readActiveSessionId,
	writeActiveSessionId,
} from "@/lib/claw-active-session";
import {
	fetchSessionReplaySnapshot,
	readCachedSessionReplay,
} from "@/lib/claw-session-cache";
import { useClawSessionReplay } from "@/lib/claw-session-replay";
import { pushHomeUrl, pushSessionUrl } from "@/lib/claw-session-url";

type ThreadListSidebarProps = React.ComponentProps<typeof Sidebar> & {
	account: ClawAccount;
	onLogout: () => void;
};

type ClawState = {
	model?: string;
	session_directory?: string;
	active_session_id?: string | null;
};

type ClawSessionSummary = {
	session_id: string;
	turns: number;
	tool_calls: number;
	modified_at: number;
	preview?: string;
	model?: string;
	usage?: UsageSummary;
	is_training?: boolean;
};

type UsageSummary = {
	input_tokens?: number;
	output_tokens?: number;
	total_tokens?: number;
};

type ClawStoredSession = {
	session_id: string;
	messages?: unknown[];
	usage?: UsageSummary;
	tool_calls?: number;
	turns?: number;
	model?: string;
};

type UserMemoryPayload = {
	content: string;
	line_count?: number;
	updated_at?: string | null;
};

type SkillMemoryPayload = {
	skill: string;
	content: string;
	line_count?: number;
	updated_at?: string | null;
};

type MemoryQueuePayload = {
	queue?: {
		totals?: {
			events?: number;
			pending?: number;
			processing?: number;
			done?: number;
			failed?: number;
		};
	};
	events?: MemoryEventPayload[];
};

type MemoryEventPayload = {
	id: string;
	session_id: string;
	status: string;
	priority: number;
	signals?: string[];
	error?: string | null;
	updated_at?: string;
};

type ReplaySessionPayload = Parameters<typeof toReplayRepository>[0];

type SidebarSession = ClawSessionSummary & {
	preview: string;
};

export function ThreadListSidebar({
	account,
	onLogout,
	...props
}: ThreadListSidebarProps) {
	return (
		<Sidebar collapsible="icon" {...props}>
			<div className="flex h-full flex-col group-data-[collapsible=icon]:hidden">
				<SidebarHeader className="aui-sidebar-header mb-2 border-b">
					<div className="aui-sidebar-header-content flex items-center justify-between gap-2">
						<SidebarMenu>
							<SidebarMenuItem>
								<SidebarMenuButton size="lg">
									<div className="aui-sidebar-header-icon-wrapper flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-accent text-sidebar-foreground">
										<BotIcon className="aui-sidebar-header-icon size-4" />
									</div>
									<div className="aui-sidebar-header-heading mr-6 flex flex-col gap-0.5 leading-none">
										<span className="aui-sidebar-header-title font-semibold">
											ZK Data Agent
										</span>
										<span className="text-muted-foreground text-xs">
											中控数据开发平台
										</span>
									</div>
								</SidebarMenuButton>
							</SidebarMenuItem>
						</SidebarMenu>
						<SidebarTrigger className="size-8 shrink-0" />
					</div>
				</SidebarHeader>
				<SidebarContent className="aui-sidebar-content overflow-hidden px-2">
					<ThreadList />
				</SidebarContent>
				<SidebarRail />
				<SidebarFooter className="aui-sidebar-footer border-t">
					<AccountMenu account={account} onLogout={onLogout} />
				</SidebarFooter>
			</div>
			<div className="hidden h-full group-data-[collapsible=icon]:flex">
				<CollapsedSidebarRail account={account} onLogout={onLogout} />
			</div>
		</Sidebar>
	);
}

function CollapsedSidebarRail({
	account,
	onLogout,
}: {
	account: ClawAccount;
	onLogout: () => void;
}) {
	const { setOpen } = useSidebar();
	const { clearSession } = useClawSessionReplay();

	return (
		<div className="flex h-full w-full flex-col items-center border-r bg-sidebar py-3 text-sidebar-foreground">
			<button
				type="button"
				className="group mb-5 flex size-9 items-center justify-center rounded-lg text-sidebar-foreground transition-colors hover:bg-sidebar-accent"
				aria-label="展开侧栏"
				title="展开侧栏"
				onClick={() => setOpen(true)}
			>
				<BotIcon className="size-5 group-hover:hidden" />
				<PanelLeftOpenIcon className="hidden size-5 group-hover:block" />
			</button>

			<div className="flex flex-col items-center gap-3">
				<CollapsedIconButton
					label="新任务"
					onClick={() => {
						pushHomeUrl();
						clearActiveSessionId();
						clearSession();
						window.dispatchEvent(new Event("claw-active-session-cleared"));
					}}
				>
					<SquarePenIcon className="size-4" />
				</CollapsedIconButton>
				<CollapsedSessionSearch />
				<CollapsedRecentSessions />
			</div>

			<div className="mt-auto">
				<AccountMenu account={account} onLogout={onLogout} compact />
			</div>
		</div>
	);
}

function CollapsedIconButton({
	label,
	children,
	onClick,
}: {
	label: string;
	children: React.ReactNode;
	onClick?: () => void;
}) {
	return (
		<button
			type="button"
			className="flex size-8 items-center justify-center rounded-lg text-sidebar-foreground transition-colors hover:bg-sidebar-accent"
			aria-label={label}
			title={label}
			onClick={onClick}
		>
			{children}
		</button>
	);
}

function CollapsedSessionSearch() {
	const { replaySession } = useClawSessionReplay();
	const [open, setOpen] = useState(false);
	const [query, setQuery] = useState("");
	const { sessions, loadingSessionId, openSession } = useSidebarSessions(open);
	const filteredSessions = sessions.filter((session) => {
		const keyword = query.trim().toLowerCase();
		if (!keyword) return true;
		return (
			session.preview.toLowerCase().includes(keyword) ||
			session.session_id.toLowerCase().includes(keyword)
		);
	});

	return (
		<>
			<CollapsedIconButton label="搜索任务" onClick={() => setOpen(true)}>
				<SearchIcon className="size-4" />
			</CollapsedIconButton>
			<Dialog open={open} onOpenChange={setOpen}>
				<DialogContent className="top-[35%] max-w-lg translate-y-[-35%] gap-3 p-4">
					<DialogHeader>
						<DialogTitle>搜索任务</DialogTitle>
						<DialogDescription>
							按任务标题或会话 ID 搜索，点击后恢复会话。
						</DialogDescription>
					</DialogHeader>
					<Input
						autoFocus
						value={query}
						placeholder="输入关键词..."
						onChange={(event) => setQuery(event.target.value)}
					/>
					<div className="max-h-80 overflow-y-auto">
						{filteredSessions.length ? (
							<div className="flex flex-col gap-1">
								{filteredSessions.map((session) => (
									<SessionResultButton
										key={session.session_id}
										session={session}
										loading={loadingSessionId === session.session_id}
										onClick={async () => {
											await openSession(session.session_id, replaySession);
											setOpen(false);
										}}
									/>
								))}
							</div>
						) : (
							<p className="py-8 text-center text-muted-foreground text-sm">
								没有匹配的任务。
							</p>
						)}
					</div>
				</DialogContent>
			</Dialog>
		</>
	);
}

function CollapsedRecentSessions() {
	const { replaySession } = useClawSessionReplay();
	const [open, setOpen] = useState(false);
	const { sessions, loadingSessionId, openSession } = useSidebarSessions(open);

	return (
		<PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
			<PopoverPrimitive.Trigger asChild>
				<CollapsedIconButton label="最近任务">
					<Clock3Icon className="size-4" />
				</CollapsedIconButton>
			</PopoverPrimitive.Trigger>
			<PopoverPrimitive.Portal>
				<PopoverPrimitive.Content
					side="right"
					align="start"
					sideOffset={10}
					className="z-50 w-72 rounded-xl border bg-popover p-2 text-popover-foreground shadow-lg outline-none"
				>
					<div className="px-2 py-1.5 font-medium text-sm">最近任务</div>
					{sessions.length ? (
						<div className="flex max-h-96 flex-col gap-1 overflow-y-auto">
							{sessions.map((session) => (
								<SessionResultButton
									key={session.session_id}
									session={session}
									loading={loadingSessionId === session.session_id}
									onClick={async () => {
										await openSession(session.session_id, replaySession);
										setOpen(false);
									}}
								/>
							))}
						</div>
					) : (
						<p className="px-2 py-6 text-center text-muted-foreground text-sm">
							暂无最近任务。
						</p>
					)}
				</PopoverPrimitive.Content>
			</PopoverPrimitive.Portal>
		</PopoverPrimitive.Root>
	);
}

function SessionResultButton({
	session,
	loading,
	onClick,
}: {
	session: SidebarSession;
	loading: boolean;
	onClick: () => void;
}) {
	return (
		<button
			type="button"
			className="flex w-full flex-col rounded-lg px-2 py-2 text-left transition-colors hover:bg-muted"
			onClick={onClick}
		>
			<span className="flex items-center gap-1.5">
				<span className="truncate font-medium text-sm">
					{loading ? "回放中..." : session.preview || session.session_id}
				</span>
				{session.is_training ? (
					<span className="shrink-0 rounded bg-amber-100 px-1.5 text-[10px] text-amber-800">
						训练
					</span>
				) : null}
			</span>
			<span className="mt-0.5 text-muted-foreground text-xs">
				{session.turns ?? 0} 轮 · {session.tool_calls ?? 0} 次工具
			</span>
		</button>
	);
}

function useSidebarSessions(open: boolean) {
	const [sessions, setSessions] = useState<SidebarSession[]>([]);
	const [loadingSessionId, setLoadingSessionId] = useState<string | null>(null);
	const openRequestRef = useRef(0);
	const openAbortRef = useRef<AbortController | null>(null);

	useEffect(() => {
		if (!open) return;
		let cancelled = false;
		const reload = () => {
			fetch("/api/claw/sessions", { cache: "no-store" })
				.then((res) => (res.ok ? res.json() : []))
				.then((payload) => {
					if (cancelled || !Array.isArray(payload)) return;
					setSessions(dedupeSidebarSessions(payload));
				})
				.catch(() => {
					if (!cancelled) setSessions([]);
				});
		};
		reload();
		const handleUpdate = () => reload();
		window.addEventListener("claw-session-updated", handleUpdate);
		return () => {
			cancelled = true;
			window.removeEventListener("claw-session-updated", handleUpdate);
		};
	}, [open]);

	async function openSession(
		sessionId: string,
		replaySession: (
			sessionId: string,
			repository: ExportedMessageRepository,
		) => void,
	) {
		const cleanSessionId = normalizeSessionId(sessionId);
		if (!cleanSessionId) return;
		const requestId = openRequestRef.current + 1;
		openRequestRef.current = requestId;
		openAbortRef.current?.abort();
		const abortController = new AbortController();
		openAbortRef.current = abortController;
		const cached = readCachedSessionReplay<ReplaySessionPayload, ClawRunStatus>(
			cleanSessionId,
		);
		writeActiveSessionId(cleanSessionId);
		pushSessionUrl(cleanSessionId);
		if (cached) {
			replaySession(
				cleanSessionId,
				toReplayRepository(cached.session, cleanSessionId, cached.runStatus),
			);
			setLoadingSessionId(null);
		} else {
			setLoadingSessionId(sessionId);
		}
		try {
			const snapshot = await fetchSessionReplaySnapshot<
				ReplaySessionPayload,
				ClawRunStatus
			>(cleanSessionId, { signal: abortController.signal });
			if (requestId !== openRequestRef.current) return;
			if (readActiveSessionId() !== cleanSessionId) return;
			if (!snapshot) return;
			if (!cached || snapshot.signature !== cached.signature) {
				replaySession(
					cleanSessionId,
					toReplayRepository(
						snapshot.session,
						cleanSessionId,
						snapshot.runStatus,
					),
				);
			}
		} catch (error) {
			if (!(error instanceof DOMException && error.name === "AbortError")) {
				console.warn("[sidebar] failed to open session", error);
			}
		} finally {
			if (requestId === openRequestRef.current) {
				setLoadingSessionId(null);
			}
			if (openAbortRef.current === abortController) {
				openAbortRef.current = null;
			}
		}
	}

	return { sessions, loadingSessionId, openSession };
}

function dedupeSidebarSessions(payload: unknown[]) {
	const byId = new Map<string, SidebarSession>();
	for (const item of payload) {
		if (!isSidebarSession(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);
}

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

function AccountMenu({
	account,
	onLogout,
	compact = false,
}: {
	account: ClawAccount;
	onLogout: () => void;
	compact?: boolean;
}) {
	const [open, setOpen] = useState(false);
	const [state, setState] = useState<ClawState | null>(null);
	const [sessions, setSessions] = useState<ClawSessionSummary[]>([]);
	const [activeSession, setActiveSession] = useState<ClawStoredSession | null>(
		null,
	);
	const [modelUsageOpen, setModelUsageOpen] = useState(false);
	const [memoryDialogOpen, setMemoryDialogOpen] = useState(false);
	const [userMemory, setUserMemory] = useState<UserMemoryPayload | null>(null);
	const [skillMemories, setSkillMemories] = useState<SkillMemoryPayload[]>([]);
	const [memoryQueue, setMemoryQueue] = useState<MemoryQueuePayload | null>(
		null,
	);
	const [memoryDraft, setMemoryDraft] = useState("");
	const [selectedMemorySkill, setSelectedMemorySkill] = useState("");
	const [memorySaving, setMemorySaving] = useState(false);

	useEffect(() => {
		if (!open) return;
		Promise.all([
			fetch("/api/claw/state", { cache: "no-store" }).then((res) =>
				res.ok ? res.json() : null,
			),
			fetch("/api/claw/sessions", { cache: "no-store" }).then((res) =>
				res.ok ? res.json() : [],
			),
		])
			.then(async ([statePayload, sessionsPayload]) => {
				const nextState = statePayload as ClawState | null;
				const nextSessions = Array.isArray(sessionsPayload)
					? (sessionsPayload as ClawSessionSummary[])
					: [];
				setState(nextState);
				setSessions(nextSessions);
				const activeSessionId = normalizeSessionId(readActiveSessionId());
				if (!activeSessionId) {
					setActiveSession(null);
					return;
				}
				writeActiveSessionId(activeSessionId);
				const response = await fetch(`/api/claw/sessions/${activeSessionId}`, {
					cache: "no-store",
				});
				setActiveSession(response.ok ? await response.json() : null);
			})
			.catch(() => {
				setState(null);
				setSessions([]);
				setActiveSession(null);
			});
	}, [open]);

	useEffect(() => {
		if (!open && !memoryDialogOpen) return;
		Promise.all([
			fetch("/api/claw/memory/user", { cache: "no-store" }).then((res) =>
				res.ok ? res.json() : null,
			),
			fetch("/api/claw/memory/skills", { cache: "no-store" }).then((res) =>
				res.ok ? res.json() : [],
			),
			fetch("/api/claw/memory/events", { cache: "no-store" }).then((res) =>
				res.ok ? res.json() : null,
			),
		]).then(([userPayload, skillPayload, queuePayload]) => {
			const nextUser = userPayload as UserMemoryPayload | null;
			setUserMemory(nextUser);
			setSkillMemories(
				Array.isArray(skillPayload)
					? (skillPayload as SkillMemoryPayload[])
					: [],
			);
			setMemoryQueue(queuePayload as MemoryQueuePayload | null);
			if (memoryDialogOpen && !selectedMemorySkill) {
				setMemoryDraft(nextUser?.content ?? "");
			}
		});
	}, [open, memoryDialogOpen, selectedMemorySkill]);

	async function logout() {
		await fetch("/api/claw/auth/logout", { method: "POST" });
		clearActiveSessionId();
		onLogout();
	}

	async function saveUserMemory() {
		setMemorySaving(true);
		try {
			const response = await fetch(
				selectedMemorySkill
					? `/api/claw/memory/skills/${encodeURIComponent(selectedMemorySkill)}`
					: "/api/claw/memory/user",
				{
					method: "PUT",
					headers: { "content-type": "application/json" },
					body: JSON.stringify({ content: memoryDraft }),
				},
			);
			if (response.ok) {
				if (selectedMemorySkill) {
					const payload = (await response.json()) as SkillMemoryPayload;
					setSkillMemories((current) =>
						current.some((item) => item.skill === payload.skill)
							? current.map((item) =>
									item.skill === payload.skill ? payload : item,
								)
							: [...current, payload],
					);
					setMemoryDraft(payload.content);
				} else {
					const payload = (await response.json()) as UserMemoryPayload;
					setUserMemory(payload);
					setMemoryDraft(payload.content);
				}
			}
		} finally {
			setMemorySaving(false);
		}
	}

	const totalToolCalls = sessions.reduce(
		(sum, session) => sum + (session.tool_calls ?? 0),
		0,
	);
	const modelUsage = aggregateUsageByModel(sessions);
	const modelUsageTotalTokens = modelUsage.reduce(
		(sum, item) => sum + item.totalTokens,
		0,
	);
	const selectedSkillMemory = selectedMemorySkill
		? skillMemories.find((item) => item.skill === selectedMemorySkill)
		: null;

	return (
		<PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
			<PopoverPrimitive.Trigger asChild>
				{compact ? (
					<button
						type="button"
						className="flex size-8 items-center justify-center rounded-full bg-sidebar-accent text-sidebar-foreground transition-colors hover:bg-sidebar-accent/80 hover:text-sidebar-accent-foreground"
						aria-label="账号与统计"
						title={account.username}
					>
						<span className="font-medium text-xs">
							{account.username.slice(0, 2).toUpperCase()}
						</span>
					</button>
				) : (
					<button
						type="button"
						className="flex w-full items-center gap-2 rounded-lg px-2 py-2 text-left transition-colors hover:bg-sidebar-accent"
					>
						<div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-sidebar-accent text-sidebar-foreground">
							<UserIcon className="size-4" />
						</div>
						<div className="min-w-0 flex-1">
							<div className="truncate font-medium text-sm">
								{account.username}
							</div>
							<div className="text-muted-foreground text-xs">账号与统计</div>
						</div>
					</button>
				)}
			</PopoverPrimitive.Trigger>
			<PopoverPrimitive.Portal>
				<PopoverPrimitive.Content
					side="top"
					align="start"
					sideOffset={8}
					className="z-50 w-80 rounded-lg border bg-popover p-3 text-popover-foreground shadow-md outline-none"
				>
					<div className="mb-3 flex items-center gap-2">
						<div className="flex size-9 items-center justify-center rounded-lg bg-primary text-primary-foreground">
							<UserIcon className="size-4" />
						</div>
						<div className="min-w-0 flex-1">
							<div className="truncate font-medium">{account.username}</div>
							<div className="truncate text-muted-foreground text-xs">
								{state?.model ?? "模型配置读取中"}
							</div>
						</div>
						<Button
							variant="ghost"
							size="sm"
							className="h-8 shrink-0 gap-1.5 px-2 text-muted-foreground"
							onClick={logout}
						>
							<LogOutIcon className="size-3.5" />
							退出
						</Button>
					</div>
					<div className="grid grid-cols-3 gap-2">
						<AccountStat label="会话" value={sessions.length} />
						<AccountStat label="工具" value={totalToolCalls} />
						<AccountStat
							label="Token"
							value={activeSession?.usage?.total_tokens ?? "暂无"}
						/>
					</div>
					<div className="mt-3 rounded-md border bg-muted/30 p-2 text-xs">
						<div className="mb-1 flex items-center gap-1.5 font-medium">
							<BarChart3Icon className="size-3.5" />
							当前会话
						</div>
						<div className="grid grid-cols-2 gap-x-3 gap-y-1 text-muted-foreground">
							<span>输入 token</span>
							<span className="text-right">
								{activeSession?.usage?.input_tokens ?? "暂无"}
							</span>
							<span>输出 token</span>
							<span className="text-right">
								{activeSession?.usage?.output_tokens ?? "暂无"}
							</span>
							<span>工具调用</span>
							<span className="text-right">
								{activeSession?.tool_calls ?? "暂无"}
							</span>
						</div>
					</div>
					<div className="mt-3 rounded-md border bg-muted/30 p-2 text-xs">
						<button
							type="button"
							className="flex w-full items-center justify-between gap-2 text-left"
							onClick={() => setModelUsageOpen((value) => !value)}
						>
							<span className="font-medium">按模型统计</span>
							<span className="flex items-center gap-1 text-muted-foreground">
								{modelUsage.length
									? `${modelUsage.length} 个模型 · ${formatCompactNumber(modelUsageTotalTokens)} tokens`
									: "暂无"}
								<ChevronDownIcon
									className={`size-3.5 transition-transform ${
										modelUsageOpen ? "rotate-180" : ""
									}`}
								/>
							</span>
						</button>
						{modelUsageOpen && modelUsage.length ? (
							<div className="mt-2 flex max-h-48 flex-col gap-2 overflow-y-auto pr-1">
								{modelUsage.map((item) => (
									<div
										key={item.model}
										className="grid grid-cols-[1fr_auto] gap-2"
									>
										<span className="truncate text-muted-foreground">
											{item.model}
										</span>
										<span>{formatCompactNumber(item.totalTokens)} tokens</span>
										<span className="text-muted-foreground">
											{item.sessions} 会话 · {item.toolCalls} 工具
										</span>
										<span className="text-right text-muted-foreground">
											in {formatCompactNumber(item.inputTokens)} / out{" "}
											{formatCompactNumber(item.outputTokens)}
										</span>
									</div>
								))}
							</div>
						) : null}
						{modelUsageOpen && !modelUsage.length ? (
							<div className="text-muted-foreground">暂无可统计会话</div>
						) : null}
					</div>
					<div className="mt-3 rounded-md border bg-muted/30 p-2 text-xs">
						<div className="flex items-center justify-between gap-2">
							<span className="flex items-center gap-1.5 font-medium">
								<BrainIcon className="size-3.5" />
								记忆
							</span>
							<Button
								size="xs"
								variant="outline"
								onClick={() => {
									setSelectedMemorySkill("");
									setMemoryDraft(userMemory?.content ?? "");
									setOpen(false);
									setMemoryDialogOpen(true);
								}}
							>
								管理
							</Button>
						</div>
						<div className="mt-2 grid grid-cols-3 gap-2">
							<AccountStat label="用户行" value={userMemory?.line_count ?? 0} />
							<AccountStat label="Skill" value={skillMemories.length} />
							<AccountStat
								label="待处理"
								value={memoryQueue?.queue?.totals?.pending ?? 0}
							/>
						</div>
					</div>
					<div className="mt-3 flex items-center justify-between gap-2 border-t pt-3">
						<span className="text-muted-foreground text-xs">
							界面与模型配置
						</span>
						<div className="flex items-center gap-1">
							<ClawThemeSwitcher />
							<ClawLlmSettings />
						</div>
					</div>
				</PopoverPrimitive.Content>
			</PopoverPrimitive.Portal>
			<Dialog open={memoryDialogOpen} onOpenChange={setMemoryDialogOpen}>
				<DialogContent className="max-h-[88vh] overflow-hidden sm:max-w-5xl">
					<DialogHeader>
						<DialogTitle>个性化记忆</DialogTitle>
						<DialogDescription>
							用户记忆会全局注入；Skill 记忆按 Skill 文件保存，后续按启用的
							Skill 注入。
						</DialogDescription>
					</DialogHeader>
					<div className="grid min-h-0 gap-4 md:grid-cols-[220px_1fr_240px]">
						<div className="min-h-0 rounded-md border bg-muted/20">
							<div className="border-b px-3 py-2 font-medium text-xs">
								记忆文件
							</div>
							<div className="max-h-[520px] overflow-y-auto p-2">
								<Button
									className="mb-2 h-auto w-full justify-start px-2 py-2 text-left"
									size="sm"
									variant={selectedMemorySkill ? "outline" : "secondary"}
									onClick={() => {
										setSelectedMemorySkill("");
										setMemoryDraft(userMemory?.content ?? "");
									}}
								>
									<div className="min-w-0">
										<div className="truncate font-medium">用户记忆</div>
										<div className="text-muted-foreground text-xs">
											{userMemory?.line_count ?? 0} 行
										</div>
									</div>
								</Button>
								<div className="mb-1 px-1 text-muted-foreground text-xs">
									Skill 记忆
								</div>
								{skillMemories.length ? (
									<div className="grid gap-1">
										{skillMemories.map((item) => (
											<Button
												key={item.skill}
												className="h-auto w-full justify-start px-2 py-2 text-left"
												size="sm"
												variant={
													selectedMemorySkill === item.skill
														? "secondary"
														: "ghost"
												}
												onClick={() => {
													setSelectedMemorySkill(item.skill);
													setMemoryDraft(item.content);
												}}
											>
												<div className="min-w-0">
													<div className="truncate font-medium">
														{item.skill}
													</div>
													<div className="text-muted-foreground text-xs">
														{item.line_count ?? 0} 行
														{item.updated_at
															? ` · ${formatShortTime(item.updated_at)}`
															: ""}
													</div>
												</div>
											</Button>
										))}
									</div>
								) : (
									<div className="rounded-md border border-dashed p-3 text-muted-foreground text-xs">
										还没有 Skill 记忆。后续使用 Skill
										时，如果出现可沉淀经验，会按 Skill 单独生成文件。
									</div>
								)}
							</div>
						</div>
						<div className="grid min-h-0 content-start gap-2">
							<div className="rounded-md border bg-muted/20 px-3 py-2">
								<div className="font-medium text-sm">
									{selectedMemorySkill
										? `${selectedMemorySkill} 使用记忆`
										: "用户记忆"}
								</div>
								<div className="mt-1 text-muted-foreground text-xs">
									{selectedMemorySkill
										? "仅在该 Skill 启用并进入模型可见列表时注入。"
										: "作为当前账号的长期偏好和通用经验注入。"}
								</div>
							</div>
							<textarea
								className="min-h-[420px] resize-none rounded-md border bg-background p-3 font-mono text-xs outline-none"
								value={memoryDraft}
								onChange={(event) => setMemoryDraft(event.target.value)}
							/>
							<div className="flex items-center justify-between gap-2">
								<span className="truncate text-muted-foreground text-xs">
									{selectedMemorySkill && selectedSkillMemory?.updated_at
										? `最近更新 ${formatShortTime(selectedSkillMemory.updated_at)}`
										: userMemory?.updated_at
											? `最近更新 ${formatShortTime(userMemory.updated_at)}`
											: "暂无更新时间"}
								</span>
								<Button
									size="sm"
									onClick={saveUserMemory}
									disabled={memorySaving}
								>
									<SaveIcon className="size-3.5" />
									{memorySaving ? "保存中..." : "保存"}
								</Button>
							</div>
						</div>
						<div className="grid content-start gap-3">
							<div className="grid grid-cols-2 gap-2">
								<AccountStat
									label="用户记忆行"
									value={userMemory?.line_count ?? 0}
								/>
								<AccountStat label="Skill 记忆" value={skillMemories.length} />
								<AccountStat
									label="待处理"
									value={memoryQueue?.queue?.totals?.pending ?? 0}
								/>
								<AccountStat
									label="失败"
									value={memoryQueue?.queue?.totals?.failed ?? 0}
								/>
							</div>
							<div className="rounded-md border bg-muted/30">
								<div className="border-b px-2 py-2 font-medium text-xs">
									最近记忆事件
								</div>
								<div className="max-h-[320px] overflow-y-auto p-2">
									{memoryQueue?.events?.length ? (
										memoryQueue.events.map((event) => (
											<div
												key={event.id}
												className="mb-2 rounded-md border bg-background p-2 text-xs"
											>
												<div className="flex items-center justify-between gap-2">
													<span className="font-medium">{event.status}</span>
													<span className="text-muted-foreground">
														P{event.priority}
													</span>
												</div>
												<div className="mt-1 truncate text-muted-foreground">
													{event.session_id}
												</div>
												<div className="mt-1 text-muted-foreground">
													{event.signals?.join(" / ") || "无信号"}
												</div>
												{event.error ? (
													<div className="mt-1 text-destructive">
														{event.error}
													</div>
												) : null}
											</div>
										))
									) : (
										<div className="py-6 text-center text-muted-foreground text-xs">
											暂无事件
										</div>
									)}
								</div>
							</div>
						</div>
					</div>
				</DialogContent>
			</Dialog>
		</PopoverPrimitive.Root>
	);
}

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

function aggregateUsageByModel(sessions: ClawSessionSummary[]) {
	const byModel = new Map<
		string,
		{
			model: string;
			sessions: number;
			toolCalls: number;
			inputTokens: number;
			outputTokens: number;
			totalTokens: number;
		}
	>();
	for (const session of sessions) {
		const model = session.model || "unknown";
		const current = byModel.get(model) ?? {
			model,
			sessions: 0,
			toolCalls: 0,
			inputTokens: 0,
			outputTokens: 0,
			totalTokens: 0,
		};
		current.sessions += 1;
		current.toolCalls += session.tool_calls ?? 0;
		current.inputTokens += session.usage?.input_tokens ?? 0;
		current.outputTokens += session.usage?.output_tokens ?? 0;
		current.totalTokens +=
			session.usage?.total_tokens ??
			(session.usage?.input_tokens ?? 0) + (session.usage?.output_tokens ?? 0);
		byModel.set(model, current);
	}
	return [...byModel.values()].sort((a, b) => b.totalTokens - a.totalTokens);
}

function formatCompactNumber(value: number) {
	return new Intl.NumberFormat("en", {
		notation: "compact",
		maximumFractionDigits: 1,
	}).format(value);
}

function formatShortTime(value: string) {
	const timestamp = Date.parse(value);
	if (Number.isNaN(timestamp)) return value;
	return new Intl.DateTimeFormat("zh-CN", {
		month: "2-digit",
		day: "2-digit",
		hour: "2-digit",
		minute: "2-digit",
	}).format(new Date(timestamp));
}

function AccountStat({
	label,
	value,
}: {
	label: string;
	value: string | number;
}) {
	return (
		<div className="rounded-md border bg-muted/30 px-2 py-1.5">
			<div className="font-medium text-sm">{value}</div>
			<div className="text-muted-foreground text-xs">{label}</div>
		</div>
	);
}
