import * as React from "react";
import type {
	AgentConfigInfo,
	AgentEvent,
	AgentStatusValue,
} from "../api/agents";
import { agentsApi, subscribeAgentStream } from "../api/agents";
import { settingsApi } from "../api/settings";
import { useLozengeFly } from "./LozengeFlyContext";

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

interface AgentState {
	agentName: string;
	status: AgentStatusValue;
	sessionId: string | null;
	worktreePath: string | null;
	baseBranch: string | null;
	terminalAt: number | null;
}

const formatTimestamp = (value: Date | null): string => {
	if (!value) return "";
	return value.toLocaleTimeString([], {
		hour: "2-digit",
		minute: "2-digit",
		second: "2-digit",
	});
};

function RosterBadge({ agent }: { agent: AgentConfigInfo }) {
	const { registerRosterEl } = useLozengeFly();
	const badgeRef = useRef<HTMLSpanElement>(null);

	// Register this element so fly animations know where to start/end
	useEffect(() => {
		const el = badgeRef.current;
		if (el) registerRosterEl(agent.name, el);
		return () => registerRosterEl(agent.name, null);
	}, [agent.name, registerRosterEl]);

	return (
		<span
			ref={badgeRef}
			data-testid={`roster-badge-${agent.name}`}
			style={{
				display: "inline-flex",
				alignItems: "center",
				gap: "4px",
				padding: "2px 8px",
				borderRadius: "6px",
				fontSize: "0.7em",
				background: "#aaaaaa18",
				color: "#aaa",
				border: "1px solid #aaaaaa44",
			}}
			title={`${agent.role || agent.name} — available`}
		>
			<span
				data-testid={`roster-dot-${agent.name}`}
				style={{
					width: "5px",
					height: "5px",
					borderRadius: "50%",
					background: "#3fb950",
					flexShrink: 0,
				}}
			/>
			<span style={{ fontWeight: 600, color: "#aaa" }}>{agent.name}</span>
			{agent.model && <span style={{ color: "#888" }}>{agent.model}</span>}
			<span style={{ color: "#888", fontStyle: "italic" }}>available</span>
		</span>
	);
}

/** Build a composite key for tracking agent state. */
function agentKey(storyId: string, agentName: string): string {
	return `${storyId}:${agentName}`;
}

interface AgentPanelProps {
	/** Increment this to trigger a re-fetch of the agent roster. */
	configVersion?: number;
	/** Increment this to trigger a re-fetch of the agent list (agent state changed). */
	stateVersion?: number;
}

export function AgentPanel({
	configVersion = 0,
	stateVersion = 0,
}: AgentPanelProps) {
	const { hiddenRosterAgents } = useLozengeFly();
	const [agents, setAgents] = useState<Record<string, AgentState>>({});
	const [roster, setRoster] = useState<AgentConfigInfo[]>([]);
	const [actionError, setActionError] = useState<string | null>(null);
	const [lastRefresh, setLastRefresh] = useState<Date | null>(null);
	const [editorCommand, setEditorCommand] = useState<string | null>(null);
	const [editorInput, setEditorInput] = useState<string>("");
	const [editingEditor, setEditingEditor] = useState(false);
	const cleanupRefs = useRef<Record<string, () => void>>({});

	// Re-fetch roster whenever configVersion changes (triggered by agent_config_changed WS event).
	useEffect(() => {
		agentsApi
			.getAgentConfig()
			.then(setRoster)
			.catch((err) => console.error("Failed to load agent config:", err));
	}, [configVersion]);

	const subscribeToAgent = useCallback((storyId: string, agentName: string) => {
		const key = agentKey(storyId, agentName);
		cleanupRefs.current[key]?.();

		const cleanup = subscribeAgentStream(
			storyId,
			agentName,
			(event: AgentEvent) => {
				setAgents((prev) => {
					const current = prev[key] ?? {
						agentName,
						status: "pending" as AgentStatusValue,
						sessionId: null,
						worktreePath: null,
						baseBranch: null,
						terminalAt: null,
					};

					switch (event.type) {
						case "status": {
							const newStatus =
								(event.status as AgentStatusValue) ?? current.status;
							const isTerminal =
								newStatus === "completed" || newStatus === "failed";
							return {
								...prev,
								[key]: {
									...current,
									status: newStatus,
									terminalAt: isTerminal
										? (current.terminalAt ?? Date.now())
										: current.terminalAt,
								},
							};
						}
						case "done":
							return {
								...prev,
								[key]: {
									...current,
									status: "completed",
									sessionId: event.session_id ?? current.sessionId,
									terminalAt: current.terminalAt ?? Date.now(),
								},
							};
						case "error":
							return {
								...prev,
								[key]: {
									...current,
									status: "failed",
									terminalAt: current.terminalAt ?? Date.now(),
								},
							};
						default:
							// output, thinking, and other events are not displayed in the sidebar.
							// Agent output streams appear in the work item detail panel instead.
							return prev;
					}
				});
			},
			() => {
				// SSE error — agent may not be streaming yet
			},
		);

		cleanupRefs.current[key] = cleanup;
	}, []);

	/** Shared helper: fetch the agent list and update state + SSE subscriptions. */
	const refreshAgents = useCallback(() => {
		agentsApi
			.listAgents()
			.then((agentList) => {
				const agentMap: Record<string, AgentState> = {};
				const now = Date.now();
				for (const a of agentList) {
					const key = agentKey(a.story_id, a.agent_name);
					const isTerminal = a.status === "completed" || a.status === "failed";
					agentMap[key] = {
						agentName: a.agent_name,
						status: a.status,
						sessionId: a.session_id,
						worktreePath: a.worktree_path,
						baseBranch: a.base_branch,
						terminalAt: isTerminal ? now : null,
					};
					if (a.status === "running" || a.status === "pending") {
						subscribeToAgent(a.story_id, a.agent_name);
					}
				}
				setAgents(agentMap);
				setLastRefresh(new Date());
			})
			.catch((err) => console.error("Failed to load agents:", err));
	}, [subscribeToAgent]);

	// Load existing agents and editor preference on mount
	useEffect(() => {
		refreshAgents();

		settingsApi
			.getEditorCommand()
			.then((s) => {
				setEditorCommand(s.editor_command);
				setEditorInput(s.editor_command ?? "");
			})
			.catch((err) => console.error("Failed to load editor command:", err));

		return () => {
			for (const cleanup of Object.values(cleanupRefs.current)) {
				cleanup();
			}
		};
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, []);

	// Re-fetch agent list when agent state changes (via WebSocket notification).
	// Skip the initial render (stateVersion=0) since the mount effect handles that.
	useEffect(() => {
		if (stateVersion > 0) {
			refreshAgents();
		}
	}, [stateVersion, refreshAgents]);

	const handleSaveEditor = async () => {
		try {
			const trimmed = editorInput.trim() || null;
			const result = await settingsApi.setEditorCommand(trimmed);
			setEditorCommand(result.editor_command);
			setEditorInput(result.editor_command ?? "");
			setEditingEditor(false);
		} catch (err) {
			const message = err instanceof Error ? err.message : String(err);
			setActionError(`Failed to save editor: ${message}`);
		}
	};

	return (
		<div
			style={{
				border: "1px solid #333",
				borderRadius: "10px",
				padding: "12px 16px",
				background: "#1f1f1f",
				display: "flex",
				flexDirection: "column",
				gap: "8px",
			}}
		>
			<div
				style={{
					display: "flex",
					alignItems: "center",
					justifyContent: "space-between",
					gap: "12px",
				}}
			>
				<div
					style={{
						display: "flex",
						alignItems: "center",
						gap: "8px",
					}}
				>
					<div style={{ fontWeight: 600 }}>Agents</div>
					{Object.values(agents).filter((a) => a.status === "running").length >
						0 && (
						<div
							style={{
								fontSize: "0.75em",
								color: "#777",
								fontFamily: "monospace",
							}}
						>
							{
								Object.values(agents).filter((a) => a.status === "running")
									.length
							}{" "}
							running
						</div>
					)}
				</div>
				{lastRefresh && (
					<div style={{ fontSize: "0.7em", color: "#555" }}>
						Loaded {formatTimestamp(lastRefresh)}
					</div>
				)}
			</div>

			{/* Editor preference */}
			<div style={{ display: "flex", alignItems: "center", gap: "6px" }}>
				<span style={{ fontSize: "0.75em", color: "#666" }}>Editor:</span>
				{editingEditor ? (
					<>
						<input
							type="text"
							value={editorInput}
							onChange={(e) => setEditorInput(e.target.value)}
							onKeyDown={(e) => {
								if (e.key === "Enter") handleSaveEditor();
								if (e.key === "Escape") setEditingEditor(false);
							}}
							placeholder="zed, code, cursor..."
							style={{
								fontSize: "0.75em",
								background: "#111",
								border: "1px solid #444",
								borderRadius: "4px",
								color: "#ccc",
								padding: "2px 6px",
								width: "120px",
							}}
						/>
						<button
							type="button"
							onClick={handleSaveEditor}
							style={{
								fontSize: "0.7em",
								padding: "2px 8px",
								borderRadius: "4px",
								border: "1px solid #238636",
								background: "#238636",
								color: "#fff",
								cursor: "pointer",
							}}
						>
							Save
						</button>
						<button
							type="button"
							onClick={() => setEditingEditor(false)}
							style={{
								fontSize: "0.7em",
								padding: "2px 8px",
								borderRadius: "4px",
								border: "1px solid #444",
								background: "none",
								color: "#888",
								cursor: "pointer",
							}}
						>
							Cancel
						</button>
					</>
				) : (
					<button
						type="button"
						onClick={() => setEditingEditor(true)}
						style={{
							fontSize: "0.75em",
							background: "none",
							border: "1px solid #333",
							borderRadius: "4px",
							color: editorCommand ? "#aaa" : "#555",
							cursor: "pointer",
							padding: "2px 8px",
							fontFamily: editorCommand ? "monospace" : "inherit",
						}}
					>
						{editorCommand ?? "Set editor..."}
					</button>
				)}
			</div>

			{/* Roster badges — agents always display in idle state here */}
			{roster.length > 0 && (
				<div
					style={{
						display: "flex",
						flexWrap: "wrap",
						gap: "4px",
					}}
				>
					{roster.map((a) => {
						const isHidden = hiddenRosterAgents.has(a.name);
						return (
							<div
								key={`roster-wrapper-${a.name}`}
								data-testid={`roster-badge-wrapper-${a.name}`}
								style={{
									overflow: "hidden",
									maxWidth: isHidden ? "0" : "300px",
									opacity: isHidden ? 0 : 1,
									transition: "max-width 0.35s ease, opacity 0.2s ease",
								}}
							>
								<RosterBadge agent={a} />
							</div>
						);
					})}
				</div>
			)}

			{actionError && (
				<div
					style={{
						fontSize: "0.85em",
						color: "#ff7b72",
						padding: "4px 8px",
						background: "#ff7b7211",
						borderRadius: "6px",
					}}
				>
					{actionError}
				</div>
			)}
		</div>
	);
}
