/// Gateway management panel shown when huskies runs in `--gateway` mode.
///
/// Provides:
/// - A cross-project pipeline status view showing active stories per project.
/// - Clicking a project card switches to it.
/// - An "Add Agent" button that generates a one-time join token.
/// - Instructions for running a build agent with the token.
/// - A list of connected agents with per-agent status, project assignment, and "Remove" buttons.
/// - Auto-refresh every 5 seconds so new agents and disconnections appear without a page reload.

import * as React from "react";
import {
	gatewayApi,
	type JoinedAgent,
	type GatewayProject,
	type AllProjectsPipeline,
	type Pipeline,
	type PipelineItem,
	type Status,
} from "../api/gateway";

/// Resolve an item's pipeline column.  Servers running the new (story 1085)
/// backend send `pipeline`; older servers only send `stage` so we fall back to
/// mapping the bucket name onto the new column vocabulary.
function itemPipeline(item: PipelineItem): Pipeline {
	if (item.pipeline) return item.pipeline;
	switch (item.stage) {
		case "current":
			return "coding";
		case "qa":
			return "qa";
		case "merge":
			return "merge";
		case "done":
			return "done";
		case "archived":
			return "archived";
		default:
			return "backlog";
	}
}

/// Resolve an item's badge.  Falls back to `merge_failure`/`blocked` on
/// legacy servers that don't yet emit `status`.
function itemStatus(item: PipelineItem): Status {
	if (item.status) return item.status;
	if (item.merge_failure) return "merge-failure";
	if (item.blocked) return "blocked";
	if (item.stage === "done") return "done";
	return "active";
}

const { useCallback, useEffect, useRef, useState } = React;

/// Seconds of silence before an agent is considered disconnected.
const DISCONNECT_THRESHOLD_SECS = 60;

/// Poll the agent list this often (milliseconds).
const POLL_INTERVAL_MS = 5_000;

type AgentStatus = "idle" | "working" | "disconnected";

/// Derive an agent's display status from its last-seen timestamp and project assignment.
function agentStatus(agent: JoinedAgent): AgentStatus {
	const nowSecs = Date.now() / 1000;
	if (nowSecs - agent.last_seen > DISCONNECT_THRESHOLD_SECS) {
		return "disconnected";
	}
	return agent.assigned_project ? "working" : "idle";
}

const STATUS_COLORS: Record<AgentStatus, string> = {
	idle: "#6e7681",
	working: "#3fb950",
	disconnected: "#f85149",
};

const STATUS_LABELS: Record<AgentStatus, string> = {
	idle: "Idle",
	working: "Working",
	disconnected: "Disconnected",
};

const PIPELINE_COLORS: Record<Pipeline, string> = {
	backlog: "#8b949e",
	coding: "#3fb950",
	qa: "#d2a679",
	merge: "#79c0ff",
	done: "#6e7681",
	closed: "#6e7681",
	archived: "#6e7681",
};

const PIPELINE_LABELS: Record<Pipeline, string> = {
	backlog: "Backlog",
	coding: "In Progress",
	qa: "QA",
	merge: "Merging",
	done: "Done",
	closed: "Closed",
	archived: "Archived",
};

/// A single story row inside a project pipeline card.
/** Render one story row in a gateway-aggregate panel: `#<id> <name>` with status badge. */
export function StoryRow({ item, mergeQueuePos }: { item: PipelineItem; mergeQueuePos?: number }) {
	const pipeline = itemPipeline(item);
	const status = itemStatus(item);
	const agentStatus = item.agent?.status;

	let color: string;
	let label: string;
	let frozenPrefix = "";

	// Frozen items keep their underlying pipeline column but get a ❄️ badge.
	// (AC 4 — story 1085, subsumes the freeze-hides-item bug.)
	if (status === "frozen") {
		color = "#79c0ff";
		label = "❄ FROZEN";
		frozenPrefix = "❄ ";
	} else if (status === "merge-failure" || status === "merge-failure-final") {
		// Done items never reach this branch — `Stage::status()` returns
		// `Status::Done` for done items (AC 4).
		if (agentStatus === "running") {
			color = "#e3b341";
			label = "⟳ RECOVERING";
		} else if (agentStatus === "pending") {
			color = "#e3b341";
			label = "⏳ QUEUED";
		} else {
			color = "#f85149";
			label = status === "merge-failure-final" ? "⛔ FAILED (FINAL)" : "✕ FAILED";
		}
	} else if (status === "blocked") {
		if (agentStatus === "running") {
			color = "#e3b341";
			label = "⟳ RECOVERING";
		} else if (agentStatus === "pending") {
			color = "#e3b341";
			label = "⏳ QUEUED";
		} else {
			color = "#f85149";
			label = "⊘ BLOCKED";
		}
	} else if (status === "review-hold") {
		color = "#d2a679";
		label = "REVIEW HOLD";
	} else if (status === "abandoned") {
		color = "#6e7681";
		label = "ABANDONED";
	} else if (status === "superseded") {
		color = "#6e7681";
		label = "SUPERSEDED";
	} else if (status === "rejected") {
		color = "#f85149";
		label = "REJECTED";
	} else if (pipeline === "merge" && agentStatus === "running") {
		color = "#58a6ff";
		label = "▶ MERGING";
	} else if (pipeline === "merge" && agentStatus === "pending") {
		color = "#e3b341";
		label = "⏳ QUEUED";
	} else if (pipeline === "merge") {
		color = "#6e7681";
		if (mergeQueuePos === 1) {
			label = "NEXT IN QUEUE";
		} else if (mergeQueuePos != null) {
			label = `awaiting-slot (#${mergeQueuePos})`;
		} else {
			label = "awaiting-slot";
		}
	} else {
		color = PIPELINE_COLORS[pipeline] ?? "#8b949e";
		label = PIPELINE_LABELS[pipeline] ?? pipeline;
	}

	const isMergeActive = pipeline === "merge" && status === "active" && agentStatus === "running";
	const idNum = item.story_id.match(/^(\d+)/)?.[1];

	return (
		<div
			style={{
				display: "flex",
				alignItems: "center",
				gap: "8px",
				padding: "4px 0",
				fontSize: "0.82em",
				background: isMergeActive ? "#58a6ff0a" : undefined,
				borderRadius: isMergeActive ? "4px" : undefined,
				paddingLeft: isMergeActive ? "4px" : undefined,
				paddingRight: isMergeActive ? "4px" : undefined,
			}}
		>
			<span
				style={{
					padding: "1px 6px",
					borderRadius: "10px",
					background: `${color}22`,
					color,
					border: `1px solid ${color}44`,
					whiteSpace: "nowrap",
					flexShrink: 0,
				}}
			>
				{label}
			</span>
			<span style={{ color: "#e6edf3", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
				{idNum && <span style={{ color: "#8b949e", fontFamily: "monospace" }}>#{idNum}{" "}</span>}
				{frozenPrefix}{item.name}
			</span>
		</div>
	);
}


function TokenDisplay({ token }: { token: string }) {
	const [copied, setCopied] = useState(false);

	const envCmd = `HUSKIES_JOIN_TOKEN=${token} huskies agent --rendezvous <CRDT_SYNC_URL>`;
	const flagCmd = `huskies agent --rendezvous <CRDT_SYNC_URL> --join-token ${token}`;

	const copyToClipboard = useCallback((text: string) => {
		void navigator.clipboard.writeText(text).then(() => {
			setCopied(true);
			setTimeout(() => setCopied(false), 2000);
		});
	}, []);

	return (
		<div
			style={{
				marginTop: "12px",
				padding: "12px 16px",
				background: "#161b22",
				border: "1px solid #238636",
				borderRadius: "8px",
				fontSize: "0.85em",
			}}
		>
			<div style={{ color: "#3fb950", fontWeight: 600, marginBottom: "8px" }}>
				Token generated — run the build agent with one of:
			</div>
			<div style={{ marginBottom: "6px" }}>
				<code
					style={{
						display: "block",
						background: "#0d1117",
						padding: "8px 10px",
						borderRadius: "4px",
						color: "#e6edf3",
						wordBreak: "break-all",
					}}
				>
					{envCmd}
				</code>
			</div>
			<div>
				<code
					style={{
						display: "block",
						background: "#0d1117",
						padding: "8px 10px",
						borderRadius: "4px",
						color: "#e6edf3",
						wordBreak: "break-all",
					}}
				>
					{flagCmd}
				</code>
			</div>
			<button
				type="button"
				onClick={() => copyToClipboard(flagCmd)}
				style={{
					marginTop: "8px",
					fontSize: "0.8em",
					padding: "3px 10px",
					borderRadius: "4px",
					border: "1px solid #444",
					background: "none",
					color: "#aaa",
					cursor: "pointer",
				}}
			>
				{copied ? "Copied!" : "Copy flag command"}
			</button>
			<div style={{ marginTop: "8px", color: "#666", fontSize: "0.85em" }}>
				This token is single-use. Generate a new one for each agent.
			</div>
		</div>
	);
}

function AgentRow({
	agent,
	projects,
	onRemove,
	onAssign,
}: {
	agent: JoinedAgent;
	projects: GatewayProject[];
	onRemove: (id: string) => void;
	onAssign: (id: string, project: string | null) => void;
}) {
	const registeredAt = new Date(agent.registered_at * 1000).toLocaleString();
	const status = agentStatus(agent);
	const statusColor = STATUS_COLORS[status];
	const statusLabel = STATUS_LABELS[status];

	return (
		<div
			data-testid={`agent-row-${agent.id}`}
			style={{
				display: "flex",
				alignItems: "center",
				gap: "12px",
				padding: "10px 14px",
				background: "#161b22",
				border: "1px solid #30363d",
				borderRadius: "8px",
				marginBottom: "8px",
			}}
		>
			<div
				style={{
					width: "8px",
					height: "8px",
					borderRadius: "50%",
					background: statusColor,
					flexShrink: 0,
				}}
				title={statusLabel}
			/>
			<div style={{ flex: 1 }}>
				<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
					<span style={{ fontWeight: 600, color: "#e6edf3" }}>{agent.label}</span>
					<span
						data-testid={`agent-status-${agent.id}`}
						style={{
							fontSize: "0.75em",
							padding: "1px 6px",
							borderRadius: "10px",
							background: `${statusColor}22`,
							color: statusColor,
							border: `1px solid ${statusColor}44`,
						}}
					>
						{statusLabel}
					</span>
				</div>
				<div style={{ fontSize: "0.8em", color: "#8b949e" }}>
					{agent.address}
				</div>
				<div style={{ fontSize: "0.75em", color: "#6e7681" }}>
					Registered {registeredAt}
					{agent.assigned_project && (
						<span style={{ marginLeft: "8px", color: "#8b949e" }}>
							· Project: {agent.assigned_project}
						</span>
					)}
				</div>
			</div>
			<select
				data-testid={`assign-agent-${agent.id}`}
				value={agent.assigned_project ?? ""}
				onChange={(e) =>
					onAssign(agent.id, e.target.value === "" ? null : e.target.value)
				}
				style={{
					fontSize: "0.8em",
					padding: "4px 8px",
					borderRadius: "4px",
					border: "1px solid #30363d",
					background: "#0d1117",
					color: "#e6edf3",
					cursor: "pointer",
				}}
			>
				<option value="">— unassigned —</option>
				{projects.map((p) => (
					<option key={p.name} value={p.name}>
						{p.name}
					</option>
				))}
			</select>
			<button
				type="button"
				data-testid={`remove-agent-${agent.id}`}
				onClick={() => onRemove(agent.id)}
				style={{
					fontSize: "0.8em",
					padding: "4px 10px",
					borderRadius: "4px",
					border: "1px solid #f85149",
					background: "none",
					color: "#f85149",
					cursor: "pointer",
				}}
			>
				Remove
			</button>
		</div>
	);
}

type TabKey = "backlog" | "in-progress" | "done" | "archived";

const TAB_STORAGE_KEY = "gateway_selected_tab";

/// Read the persisted tab from localStorage, defaulting to "in-progress".
function readStoredTab(): TabKey {
	const stored = localStorage.getItem(TAB_STORAGE_KEY);
	if (
		stored === "backlog" ||
		stored === "in-progress" ||
		stored === "done" ||
		stored === "archived"
	) {
		return stored;
	}
	return "in-progress";
}

/// Aggregate pipeline items from all projects for a given tab.
function aggregateItems(
	pipeline: AllProjectsPipeline,
	tab: TabKey,
): { project: string; items: PipelineItem[] }[] {
	return Object.entries(pipeline.projects)
		.map(([project, status]) => {
			if (status.error) return { project, items: [] };
			if (tab === "backlog") {
				return {
					project,
					items: (status.backlog ?? []).map((b) => ({
						story_id: b.story_id,
						name: b.name,
						stage: "backlog",
						pipeline: "backlog" as Pipeline,
						status: "active" as Status,
					})),
				};
			}
			if (tab === "in-progress") {
				return {
					project,
					items: (status.active ?? []).filter(
						(i) => itemPipeline(i) !== "done",
					),
				};
			}
			if (tab === "done") {
				return {
					project,
					items: (status.active ?? []).filter((i) => itemPipeline(i) === "done"),
				};
			}
			// archived
			return { project, items: status.archived ?? [] };
		})
		.filter((g) => g.items.length > 0);
}

/// Count total items across all projects for a given tab.
function tabCount(pipeline: AllProjectsPipeline, tab: TabKey): number {
	return Object.values(pipeline.projects).reduce((sum, status) => {
		if (status.error) return sum;
		if (tab === "backlog") return sum + (status.backlog_count ?? 0);
		if (tab === "in-progress") {
			return (
				sum +
				(status.active ?? []).filter((i) => itemPipeline(i) !== "done").length
			);
		}
		if (tab === "done") {
			return (
				sum + (status.active ?? []).filter((i) => itemPipeline(i) === "done").length
			);
		}
		return sum + (status.archived ?? []).length;
	}, 0);
}

/// Tab bar button.
function TabButton({
	label,
	count,
	active,
	onClick,
}: {
	label: string;
	count: number;
	active: boolean;
	onClick: () => void;
}) {
	return (
		<button
			type="button"
			onClick={onClick}
			style={{
				padding: "8px 16px",
				borderRadius: "6px 6px 0 0",
				border: "1px solid",
				borderColor: active ? "#30363d" : "transparent",
				borderBottomColor: active ? "#0d1117" : "transparent",
				background: active ? "#0d1117" : "none",
				color: active ? "#e6edf3" : "#8b949e",
				cursor: "pointer",
				fontSize: "0.9em",
				fontWeight: active ? 600 : 400,
				display: "flex",
				alignItems: "center",
				gap: "6px",
			}}
		>
			{label}
			{count > 0 && (
				<span
					style={{
						padding: "1px 6px",
						borderRadius: "10px",
						background: active ? "#21262d" : "#161b22",
						color: active ? "#e6edf3" : "#6e7681",
						fontSize: "0.8em",
					}}
				>
					{count}
				</span>
			)}
		</button>
	);
}

/// A project-labelled story row used in the aggregate tab view.
function ProjectStoryRow({
	project,
	item,
	showProject,
	mergeQueuePos,
}: {
	project: string;
	item: PipelineItem;
	showProject: boolean;
	mergeQueuePos?: number;
}) {
	return (
		<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
			{showProject && (
				<span
					style={{
						fontSize: "0.75em",
						padding: "1px 6px",
						borderRadius: "10px",
						background: "#161b22",
						color: "#8b949e",
						border: "1px solid #30363d",
						whiteSpace: "nowrap",
						flexShrink: 0,
					}}
				>
					{project}
				</span>
			)}
			<div style={{ flex: 1, minWidth: 0 }}>
				<StoryRow item={item} mergeQueuePos={mergeQueuePos} />
			</div>
		</div>
	);
}

const IN_PROGRESS_PIPELINE_LABELS: Record<"coding" | "qa" | "merge", string> = {
	coding: "Coding",
	qa: "QA",
	merge: "Merging",
};

/// In Progress tab content — items grouped by their `pipeline` column.
///
/// Frozen items appear in the column corresponding to their underlying
/// `Stage::resume_to` (server-side), so they always show up in-place.
function InProgressTabContent({
	groups,
}: {
	groups: { project: string; items: PipelineItem[] }[];
}) {
	const allItems = groups.flatMap((g) =>
		g.items.map((item) => ({ project: g.project, item })),
	);
	const multiProject = new Set(allItems.map((x) => x.project)).size > 1;

	const byPipeline = {
		coding: allItems.filter((x) => itemPipeline(x.item) === "coding"),
		qa: allItems.filter((x) => itemPipeline(x.item) === "qa"),
		merge: allItems.filter((x) => itemPipeline(x.item) === "merge"),
	};

	const pipelines = (["coding", "qa", "merge"] as const).filter(
		(p) => byPipeline[p].length > 0,
	);

	// Compute queue position among "clean" awaiting-merge items: pipeline=merge,
	// status=active, and no agent currently running.
	const mergeQueuePosMap = new Map<string, number>();
	let queuePos = 0;
	for (const { project, item } of byPipeline.merge) {
		if (itemStatus(item) === "active" && item.agent?.status !== "running") {
			queuePos += 1;
			mergeQueuePosMap.set(`${project}:${item.story_id}`, queuePos);
		}
	}

	if (allItems.length === 0) {
		return (
			<p style={{ color: "#6e7681", padding: "16px 0" }}>
				No items in progress.
			</p>
		);
	}

	return (
		<div>
			{pipelines.map((p) => (
				<div key={p} style={{ marginBottom: "20px" }}>
					<div
						style={{
							fontSize: "0.8em",
							fontWeight: 600,
							color: PIPELINE_COLORS[p] ?? "#8b949e",
							textTransform: "uppercase",
							letterSpacing: "0.06em",
							marginBottom: "8px",
							paddingBottom: "4px",
							borderBottom: `1px solid ${PIPELINE_COLORS[p] ?? "#8b949e"}33`,
						}}
					>
						{IN_PROGRESS_PIPELINE_LABELS[p]}{" "}
						<span style={{ color: "#6e7681" }}>
							({byPipeline[p].length})
						</span>
					</div>
					{byPipeline[p].map(({ project, item }) => (
						<ProjectStoryRow
							key={`${project}:${item.story_id}`}
							project={project}
							item={item}
							showProject={multiProject}
							mergeQueuePos={
								p === "merge"
									? mergeQueuePosMap.get(`${project}:${item.story_id}`)
									: undefined
							}
						/>
					))}
				</div>
			))}
		</div>
	);
}

/// Flat list tab content for Backlog, Done, and Archived.
function FlatTabContent({
	groups,
	emptyMessage,
}: {
	groups: { project: string; items: PipelineItem[] }[];
	emptyMessage: string;
}) {
	const allItems = groups.flatMap((g) =>
		g.items.map((item) => ({ project: g.project, item })),
	);
	const multiProject = new Set(allItems.map((x) => x.project)).size > 1;

	if (allItems.length === 0) {
		return (
			<p style={{ color: "#6e7681", padding: "16px 0" }}>{emptyMessage}</p>
		);
	}

	return (
		<div>
			{allItems.map(({ project, item }) => (
				<ProjectStoryRow
					key={`${project}:${item.story_id}`}
					project={project}
					item={item}
					showProject={multiProject}
				/>
			))}
		</div>
	);
}

/// Gateway management panel — rendered when running in `--gateway` mode.
export function GatewayPanel() {
	const [agents, setAgents] = useState<JoinedAgent[]>([]);
	const [projects, setProjects] = useState<GatewayProject[]>([]);
	const [token, setToken] = useState<string | null>(null);
	const [generating, setGenerating] = useState(false);
	const [error, setError] = useState<string | null>(null);
	const [pipeline, setPipeline] = useState<AllProjectsPipeline | null>(null);
	const [selectedTab, setSelectedTab] = useState<TabKey>(readStoredTab);

	// Keep stable refs so polling intervals don't recreate on state changes.
	const setAgentsRef = useRef(setAgents);
	setAgentsRef.current = setAgents;
	const setPipelineRef = useRef(setPipeline);
	setPipelineRef.current = setPipeline;

	useEffect(() => {
		// Initial load.
		gatewayApi
			.listAgents()
			.then(setAgents)
			.catch(() => setAgents([]));
		gatewayApi
			.getGatewayInfo()
			.then((info) => setProjects(info.projects))
			.catch(() => setProjects([]));
		gatewayApi
			.getAllProjectsPipeline()
			.then(setPipeline)
			.catch(() => setPipeline(null));

		// Poll so the dashboard auto-updates as agents connect/disconnect and
		// stories move through pipelines.
		const timer = setInterval(() => {
			gatewayApi
				.listAgents()
				.then((updated) => setAgentsRef.current(updated))
				.catch(() => {});
			gatewayApi
				.getAllProjectsPipeline()
				.then((updated) => setPipelineRef.current(updated))
				.catch(() => {});
		}, POLL_INTERVAL_MS);

		return () => clearInterval(timer);
	}, []);

	const handleAddAgent = useCallback(async () => {
		setGenerating(true);
		setError(null);
		setToken(null);
		try {
			const result = await gatewayApi.generateToken();
			setToken(result.token);
		} catch (e) {
			setError(e instanceof Error ? e.message : String(e));
		} finally {
			setGenerating(false);
		}
	}, []);

	const handleRemoveAgent = useCallback(async (id: string) => {
		try {
			await gatewayApi.removeAgent(id);
			setAgents((prev) => prev.filter((a) => a.id !== id));
		} catch (e) {
			setError(e instanceof Error ? e.message : String(e));
		}
	}, []);

	const handleAssignAgent = useCallback(
		async (id: string, project: string | null) => {
			try {
				const updated = await gatewayApi.assignAgent(id, project);
				setAgents((prev) =>
					prev.map((a) => (a.id === updated.id ? updated : a)),
				);
			} catch (e) {
				setError(e instanceof Error ? e.message : String(e));
			}
		},
		[],
	);

	const handleSelectTab = useCallback((tab: TabKey) => {
		setSelectedTab(tab);
		localStorage.setItem(TAB_STORAGE_KEY, tab);
	}, []);


	return (
		<div
			style={{
				minHeight: "100vh",
				background: "#0d1117",
				color: "#e6edf3",
				padding: "32px",
				fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
			}}
		>
			<div style={{ maxWidth: "720px", margin: "0 auto" }}>
				<h1 style={{ fontSize: "1.5em", fontWeight: 700, marginBottom: "4px" }}>
					Huskies Gateway
				</h1>
				<p style={{ color: "#8b949e", marginBottom: "32px" }}>
					Manage build agents connected to this gateway.
				</p>

				{/* Cross-project pipeline tabs */}
				<section style={{ marginBottom: "32px" }}>
					{/* Tab bar */}
					<div
						style={{
							display: "flex",
							gap: "2px",
							borderBottom: "1px solid #30363d",
							marginBottom: "16px",
						}}
					>
						{(
							[
								{ key: "backlog", label: "Backlog" },
								{ key: "in-progress", label: "In Progress" },
								{ key: "done", label: "Done" },
								{ key: "archived", label: "Archived" },
							] as { key: TabKey; label: string }[]
						).map(({ key, label }) => (
							<TabButton
								key={key}
								label={label}
								count={pipeline ? tabCount(pipeline, key) : 0}
								active={selectedTab === key}
								onClick={() => handleSelectTab(key)}
							/>
						))}
					</div>

					{/* Tab content */}
					{pipeline ? (
						<>
							{selectedTab === "backlog" && (
								<FlatTabContent
									groups={aggregateItems(pipeline, "backlog")}
									emptyMessage="No items in backlog."
								/>
							)}
							{selectedTab === "in-progress" && (
								<InProgressTabContent
									groups={aggregateItems(pipeline, "in-progress")}
								/>
							)}
							{selectedTab === "done" && (
								<FlatTabContent
									groups={aggregateItems(pipeline, "done")}
									emptyMessage="No completed items."
								/>
							)}
							{selectedTab === "archived" && (
								<FlatTabContent
									groups={aggregateItems(pipeline, "archived")}
									emptyMessage="No archived items."
								/>
							)}
						</>
					) : (
						<p style={{ color: "#6e7681" }}>Loading pipeline status…</p>
					)}
				</section>

				{/* Add Agent */}
				<section style={{ marginBottom: "32px" }}>
					<h2
						style={{
							fontSize: "1.1em",
							fontWeight: 600,
							marginBottom: "12px",
							borderBottom: "1px solid #21262d",
							paddingBottom: "8px",
						}}
					>
						Add Agent
					</h2>
					<button
						type="button"
						data-testid="add-agent-button"
						onClick={handleAddAgent}
						disabled={generating}
						style={{
							padding: "8px 18px",
							borderRadius: "6px",
							border: "1px solid #238636",
							background: generating ? "#1a2f1a" : "#238636",
							color: "#fff",
							cursor: generating ? "not-allowed" : "pointer",
							fontWeight: 600,
							fontSize: "0.9em",
						}}
					>
						{generating ? "Generating…" : "Add Agent"}
					</button>
					{token && <TokenDisplay token={token} />}
				</section>

				{/* Agent list */}
				<section>
					<h2
						style={{
							fontSize: "1.1em",
							fontWeight: 600,
							marginBottom: "12px",
							borderBottom: "1px solid #21262d",
							paddingBottom: "8px",
						}}
					>
						Connected Agents{" "}
						{agents.length > 0 && (
							<span
								style={{
									fontSize: "0.8em",
									color: "#8b949e",
									fontWeight: 400,
								}}
							>
								({agents.length})
							</span>
						)}
					</h2>
					{agents.length === 0 ? (
						<p style={{ color: "#6e7681" }}>
							No agents connected yet. Click "Add Agent" to generate a join
							token.
						</p>
					) : (
						<div>
							{agents.map((agent) => (
								<AgentRow
									key={agent.id}
									agent={agent}
									projects={projects}
									onRemove={handleRemoveAgent}
									onAssign={handleAssignAgent}
								/>
							))}
						</div>
					)}
				</section>

				{/* Project management */}
				<section style={{ marginTop: "32px" }}>
					<h2
						style={{
							fontSize: "1.1em",
							fontWeight: 600,
							marginBottom: "12px",
							borderBottom: "1px solid #21262d",
							paddingBottom: "8px",
						}}
					>
						Projects{" "}
						{projects.length > 0 && (
							<span style={{ fontSize: "0.8em", color: "#8b949e", fontWeight: 400 }}>
								({projects.length})
							</span>
						)}
					</h2>

					{/* Existing projects list */}
					{projects.map((p) => (
						<div
							key={p.name}
							data-testid={`project-row-${p.name}`}
							style={{
								display: "flex",
								alignItems: "center",
								gap: "12px",
								padding: "10px 14px",
								background: "#161b22",
								border: "1px solid #30363d",
								borderRadius: "8px",
								marginBottom: "8px",
							}}
						>
							<div style={{ flex: 1 }}>
								<div style={{ fontWeight: 600, color: "#e6edf3" }}>{p.name}</div>
								<div style={{ fontSize: "0.8em", color: "#8b949e" }}>{p.url}</div>
							</div>
							</div>
					))}
				</section>

				{error && (
					<div
						style={{
							marginTop: "16px",
							padding: "10px 14px",
							background: "#f8514911",
							border: "1px solid #f85149",
							borderRadius: "6px",
							color: "#f85149",
							fontSize: "0.875em",
						}}
					>
						{error}
					</div>
				)}
			</div>
		</div>
	);
}
