import {
	Plugin,
	WorkspaceLeaf,
	WorkspaceSplit,
	Notice,
	requestUrl,
} from "obsidian";
import type { Root } from "react-dom/client";
import * as semver from "semver";
import { ChatView, VIEW_TYPE_CHAT } from "./components/chat/ChatView";
import {
	createFloatingChat,
	FloatingViewContainer,
} from "./components/chat/FloatingChatView";
import { FloatingButtonContainer } from "./components/chat/FloatingButton";
import { ChatViewRegistry } from "./shared/chat-view-registry";
import {
	createSettingsStore,
	type SettingsStore,
} from "./adapters/obsidian/settings-store.adapter";
import { AgentClientSettingTab } from "./components/settings/AgentClientSettingTab";
import { AcpAdapter } from "./adapters/acp/acp.adapter";
import {
	sanitizeArgs,
	normalizeEnvVars,
	normalizeCustomAgent,
	ensureUniqueCustomAgentIds,
} from "./shared/settings-utils";
import { parseChatFontSize } from "./shared/display-settings";
import {
	AgentEnvVar,
	GeminiAgentSettings,
	ClaudeAgentSettings,
	CodexAgentSettings,
	CustomAgentSettings,
} from "./domain/models/agent-config";
import type { SavedSessionInfo } from "./domain/models/session-info";
import { initializeLogger } from "./shared/logger";

// Re-export for backward compatibility
export type { AgentEnvVar, CustomAgentSettings };

/**
 * Send message shortcut configuration.
 * - 'enter': Enter to send, Shift+Enter for newline (default)
 * - 'cmd-enter': Cmd/Ctrl+Enter to send, Enter for newline
 */
export type SendMessageShortcut = "enter" | "cmd-enter";

/**
 * Chat view location configuration.
 * - 'right-tab': Open in right pane as tabs (default)
 * - 'right-split': Open in right pane with vertical split
 * - 'editor-tab': Open in editor area as tabs
 * - 'editor-split': Open in editor area with right split
 */
export type ChatViewLocation =
	| "right-tab"
	| "right-split"
	| "editor-tab"
	| "editor-split";

export interface AgentClientPluginSettings {
	gemini: GeminiAgentSettings;
	claude: ClaudeAgentSettings;
	codex: CodexAgentSettings;
	customAgents: CustomAgentSettings[];
	/** Default agent ID for new views (renamed from activeAgentId for multi-session) */
	defaultAgentId: string;
	autoAllowPermissions: boolean;
	autoMentionActiveNote: boolean;
	debugMode: boolean;
	nodePath: string;
	exportSettings: {
		defaultFolder: string;
		filenameTemplate: string;
		autoExportOnNewChat: boolean;
		autoExportOnCloseChat: boolean;
		openFileAfterExport: boolean;
		includeImages: boolean;
		imageLocation: "obsidian" | "custom" | "base64";
		imageCustomFolder: string;
		frontmatterTag: string;
	};
	// WSL settings (Windows only)
	windowsWslMode: boolean;
	windowsWslDistribution?: string;
	// Input behavior
	sendMessageShortcut: SendMessageShortcut;
	// View settings
	chatViewLocation: ChatViewLocation;
	// Display settings
	displaySettings: {
		autoCollapseDiffs: boolean;
		diffCollapseThreshold: number;
		maxNoteLength: number;
		maxSelectionLength: number;
		showEmojis: boolean;
		fontSize: number | null;
	};
	// Locally saved session metadata (for agents without session/list support)
	savedSessions: SavedSessionInfo[];
	// Last used model per agent (agentId → modelId)
	lastUsedModels: Record<string, string>;
	// Last used mode per agent (agentId → modeId)
	lastUsedModes: Record<string, string>;
	// Floating chat settings
	enableFloatingChat: boolean;
	floatingButtonImage: string;
	floatingWindowSize: { width: number; height: number };
	floatingWindowPosition: { x: number; y: number } | null;
	floatingButtonPosition: { x: number; y: number } | null;
}

const DEFAULT_SETTINGS: AgentClientPluginSettings = {
	claude: {
		id: "claude-code-acp",
		displayName: "Claude Code",
		apiKey: "",
		command: "",
		args: [],
		env: [],
	},
	codex: {
		id: "codex-acp",
		displayName: "Codex",
		apiKey: "",
		command: "",
		args: [],
		env: [],
	},
	gemini: {
		id: "gemini-cli",
		displayName: "Gemini CLI",
		apiKey: "",
		command: "",
		args: ["--experimental-acp"],
		env: [],
	},
	customAgents: [],
	defaultAgentId: "claude-code-acp",
	autoAllowPermissions: false,
	autoMentionActiveNote: true,
	debugMode: false,
	nodePath: "",
	exportSettings: {
		defaultFolder: "Agent Client",
		filenameTemplate: "agent_client_{date}_{time}",
		autoExportOnNewChat: false,
		autoExportOnCloseChat: false,
		openFileAfterExport: true,
		includeImages: true,
		imageLocation: "obsidian",
		imageCustomFolder: "Agent Client",
		frontmatterTag: "agent-client",
	},
	windowsWslMode: false,
	windowsWslDistribution: undefined,
	sendMessageShortcut: "enter",
	chatViewLocation: "right-tab",
	displaySettings: {
		autoCollapseDiffs: false,
		diffCollapseThreshold: 10,
		maxNoteLength: 10000,
		maxSelectionLength: 10000,
		showEmojis: true,
		fontSize: null,
	},
	savedSessions: [],
	lastUsedModels: {},
	lastUsedModes: {},
	enableFloatingChat: false,
	floatingButtonImage: "",
	floatingWindowSize: { width: 400, height: 500 },
	floatingWindowPosition: null,
	floatingButtonPosition: null,
};

export default class AgentClientPlugin extends Plugin {
	settings: AgentClientPluginSettings;
	settingsStore!: SettingsStore;

	/** Registry for all chat view containers (sidebar + floating) */
	viewRegistry = new ChatViewRegistry();

	/** Map of viewId to AcpAdapter for multi-session support */
	private _adapters: Map<string, AcpAdapter> = new Map();
	/** Floating button container (independent from chat view instances) */
	private floatingButton: FloatingButtonContainer | null = null;
	/** Map of viewId to floating chat roots and containers (legacy, being migrated to viewRegistry) */
	private floatingChatInstances: Map<
		string,
		{ root: Root; container: HTMLElement }
	> = new Map();
	/** Counter for generating unique floating chat instance IDs */
	private floatingChatCounter = 0;

	async onload() {
		await this.loadSettings();

		initializeLogger(this.settings);

		// Initialize settings store
		this.settingsStore = createSettingsStore(this.settings, this);

		this.registerView(VIEW_TYPE_CHAT, (leaf) => new ChatView(leaf, this));

		const ribbonIconEl = this.addRibbonIcon(
			"bot-message-square",
			"Open agent client",
			(_evt: MouseEvent) => {
				void this.activateView();
			},
		);
		ribbonIconEl.addClass("agent-client-ribbon-icon");

		this.addCommand({
			id: "open-chat-view",
			name: "Open agent chat",
			callback: () => {
				void this.activateView();
			},
		});

		this.addCommand({
			id: "focus-next-chat-view",
			name: "Focus next chat view",
			callback: () => {
				this.focusChatView("next");
			},
		});

		this.addCommand({
			id: "focus-previous-chat-view",
			name: "Focus previous chat view",
			callback: () => {
				this.focusChatView("previous");
			},
		});

		this.addCommand({
			id: "open-new-chat-view",
			name: "Open new chat view",
			callback: () => {
				void this.openNewChatViewWithAgent(
					this.settings.defaultAgentId,
				);
			},
		});

		// Register agent-specific commands
		this.registerAgentCommands();
		this.registerPermissionCommands();
		this.registerBroadcastCommands();

		// Floating chat window commands
		this.addCommand({
			id: "open-floating-chat",
			name: "Open floating chat window",
			checkCallback: (checking) => {
				if (!this.settings.enableFloatingChat) return false;
				if (checking) return true;
				const instances = this.getFloatingChatInstances();
				if (instances.length === 0) {
					this.openNewFloatingChat(true);
				} else if (instances.length === 1) {
					this.expandFloatingChat(instances[0]);
				} else {
					const focused = this.viewRegistry.getFocused();
					if (focused && focused.viewType === "floating") {
						focused.expand();
					} else {
						this.expandFloatingChat(
							instances[instances.length - 1],
						);
					}
				}
			},
		});

		this.addCommand({
			id: "open-new-floating-chat",
			name: "Open new floating chat window",
			checkCallback: (checking) => {
				if (!this.settings.enableFloatingChat) return false;
				if (checking) return true;
				this.openNewFloatingChat(true);
			},
		});

		this.addCommand({
			id: "minimize-floating-chat",
			name: "Minimize floating chat window",
			checkCallback: (checking) => {
				if (!this.settings.enableFloatingChat) return false;
				const focused = this.viewRegistry.getFocused();
				if (!(focused && focused.viewType === "floating")) return false;
				if (checking) return true;
				focused.collapse();
			},
		});

		this.addCommand({
			id: "close-floating-chat",
			name: "Close floating chat window",
			checkCallback: (checking) => {
				if (!this.settings.enableFloatingChat) return false;
				const focused = this.viewRegistry.getFocused();
				if (!(focused && focused.viewType === "floating")) return false;
				if (checking) return true;
				this.closeFloatingChat(focused.viewId);
			},
		});

		this.addSettingTab(new AgentClientSettingTab(this.app, this));

		// Mount floating button (always present; visibility controlled by settings inside component)
		this.floatingButton = new FloatingButtonContainer(this);
		this.floatingButton.mount();

		// Mount initial floating chat instance only if enabled
		if (this.settings.enableFloatingChat) {
			this.openNewFloatingChat();
		}

		// Clean up all ACP sessions when Obsidian quits
		// Note: We don't wait for disconnect to complete to avoid blocking quit
		this.registerEvent(
			this.app.workspace.on("quit", () => {
				// Fire and forget - don't block Obsidian from quitting
				for (const [viewId, adapter] of this._adapters) {
					adapter.disconnect().catch((error) => {
						console.warn(
							`[AgentClient] Quit cleanup error for view ${viewId}:`,
							error,
						);
					});
				}
				this._adapters.clear();
			}),
		);
	}

	onunload() {
		// Unmount floating button
		this.floatingButton?.unmount();
		this.floatingButton = null;

		// Unmount all floating chat instances via registry
		for (const container of this.viewRegistry.getByType("floating")) {
			if (container instanceof FloatingViewContainer) {
				container.unmount();
			}
		}

		// Clear registry (sidebar views are managed by Obsidian workspace)
		this.viewRegistry.clear();

		// Clear legacy storage
		this.floatingChatInstances.clear();
	}

	/**
	 * Get or create an AcpAdapter for a specific view.
	 * Each ChatView has its own adapter for independent sessions.
	 */
	getOrCreateAdapter(viewId: string): AcpAdapter {
		let adapter = this._adapters.get(viewId);
		if (!adapter) {
			adapter = new AcpAdapter(this);
			this._adapters.set(viewId, adapter);
		}
		return adapter;
	}

	/**
	 * Remove and disconnect the adapter for a specific view.
	 * Called when a ChatView is closed.
	 */
	async removeAdapter(viewId: string): Promise<void> {
		const adapter = this._adapters.get(viewId);
		if (adapter) {
			try {
				await adapter.disconnect();
			} catch (error) {
				console.warn(
					`[AgentClient] Failed to disconnect adapter for view ${viewId}:`,
					error,
				);
			}
			this._adapters.delete(viewId);
		}
		// Note: lastActiveChatViewId is now managed by viewRegistry
		// Clearing happens automatically when view is unregistered
	}

	/**
	 * Get the last active ChatView ID for keybind targeting.
	 */
	get lastActiveChatViewId(): string | null {
		return this.viewRegistry.getFocusedId();
	}

	/**
	 * Set the last active ChatView ID.
	 * Called when a ChatView receives focus or interaction.
	 */
	setLastActiveChatViewId(viewId: string | null): void {
		if (viewId) {
			this.viewRegistry.setFocused(viewId);
		}
	}

	async activateView() {
		const { workspace } = this.app;

		let leaf: WorkspaceLeaf | null = null;
		const leaves = workspace.getLeavesOfType(VIEW_TYPE_CHAT);

		if (leaves.length > 0) {
			// Find the leaf matching lastActiveChatViewId, or fall back to first leaf
			const focusedId = this.lastActiveChatViewId;
			if (focusedId) {
				leaf =
					leaves.find(
						(l) => (l.view as ChatView)?.viewId === focusedId,
					) || leaves[0];
			} else {
				leaf = leaves[0];
			}
		} else {
			leaf = this.createNewChatLeaf(false);
			if (leaf) {
				await leaf.setViewState({
					type: VIEW_TYPE_CHAT,
					active: true,
				});
			}
		}

		if (leaf) {
			await workspace.revealLeaf(leaf);
			this.focusTextarea(leaf);
		}
	}

	/**
	 * Focus the textarea in a ChatView leaf.
	 */
	private focusTextarea(leaf: WorkspaceLeaf): void {
		const viewContainerEl = leaf.view?.containerEl;
		if (viewContainerEl) {
			window.setTimeout(() => {
				const textarea = viewContainerEl.querySelector(
					"textarea.agent-client-chat-input-textarea",
				);
				if (textarea instanceof HTMLTextAreaElement) {
					textarea.focus();
				}
			}, 50);
		}
	}

	/**
	 * Focus the next or previous ChatView in the list.
	 * Uses ChatViewRegistry which includes both sidebar and floating views.
	 */
	private focusChatView(direction: "next" | "previous"): void {
		if (direction === "next") {
			this.viewRegistry.focusNext();
		} else {
			this.viewRegistry.focusPrevious();
		}
	}

	/**
	 * Create a new leaf for ChatView based on the configured location setting.
	 * @param isAdditional - true when opening additional views (e.g., Open New View)
	 */
	private createNewChatLeaf(isAdditional: boolean): WorkspaceLeaf | null {
		const { workspace } = this.app;
		const location = this.settings.chatViewLocation;

		switch (location) {
			case "right-tab":
				if (isAdditional) {
					return this.createSidebarTab("right");
				}
				return workspace.getRightLeaf(false);
			case "right-split":
				return workspace.getRightLeaf(isAdditional);
			case "editor-tab":
				return workspace.getLeaf("tab");
			case "editor-split":
				return workspace.getLeaf("split");
			default:
				return workspace.getRightLeaf(false);
		}
	}

	/**
	 * Create a new tab within an existing sidebar tab group.
	 * Uses the parent of an existing chat leaf to add a sibling tab,
	 * avoiding the vertical split caused by getRightLeaf(true).
	 */
	private createSidebarTab(side: "right" | "left"): WorkspaceLeaf | null {
		const { workspace } = this.app;
		const split =
			side === "right" ? workspace.rightSplit : workspace.leftSplit;

		// Find an existing chat leaf in this sidebar to get its tab group
		const existingLeaves = workspace.getLeavesOfType(VIEW_TYPE_CHAT);
		const sidebarLeaf = existingLeaves.find(
			(leaf) => leaf.getRoot() === split,
		);

		if (sidebarLeaf) {
			const tabGroup = sidebarLeaf.parent;
			// Index is clamped by Obsidian, so a large value appends to the end
			return workspace.createLeafInParent(
				tabGroup as unknown as WorkspaceSplit,
				Number.MAX_SAFE_INTEGER,
			);
		}

		// Fallback: no existing chat leaf in sidebar, create first one
		return side === "right"
			? workspace.getRightLeaf(false)
			: workspace.getLeftLeaf(false);
	}

	/**
	 * Open a new chat view with a specific agent.
	 * Always creates a new view (doesn't reuse existing).
	 */
	async openNewChatViewWithAgent(agentId: string): Promise<void> {
		const leaf = this.createNewChatLeaf(true);
		if (!leaf) {
			console.warn("[AgentClient] Failed to create new leaf");
			return;
		}

		await leaf.setViewState({
			type: VIEW_TYPE_CHAT,
			active: true,
			state: { initialAgentId: agentId },
		});

		await this.app.workspace.revealLeaf(leaf);

		// Focus textarea after revealing the leaf
		const viewContainerEl = leaf.view?.containerEl;
		if (viewContainerEl) {
			window.setTimeout(() => {
				const textarea = viewContainerEl.querySelector(
					"textarea.agent-client-chat-input-textarea",
				);
				if (textarea instanceof HTMLTextAreaElement) {
					textarea.focus();
				}
			}, 0);
		}
	}

	/**
	 * Open a new floating chat window.
	 * Each window is independent with its own session.
	 */
	openNewFloatingChat(
		initialExpanded = false,
		initialPosition?: { x: number; y: number },
	): void {
		// instanceId is just the counter (e.g., "0", "1", "2")
		// FloatingViewContainer will create viewId as "floating-chat-{instanceId}"
		const instanceId = String(this.floatingChatCounter++);
		const container = createFloatingChat(
			this,
			instanceId,
			initialExpanded,
			initialPosition,
		);
		// Store by viewId for consistent lookup
		this.floatingChatInstances.set(container.viewId, {
			root: null as unknown as Root,
			container: container.getContainerEl(),
		});
	}

	/**
	 * Close a specific floating chat window.
	 * @param viewId - The viewId in "floating-chat-{id}" format (from getFloatingChatInstances())
	 */
	closeFloatingChat(viewId: string): void {
		const container = this.viewRegistry.get(viewId);
		if (container && container instanceof FloatingViewContainer) {
			container.unmount();
		}
		// Also remove from legacy floatingChatInstances if present
		this.floatingChatInstances.delete(viewId);
	}

	/**
	 * Get all floating chat instance viewIds.
	 * @returns Array of viewIds in "floating-chat-{id}" format
	 */
	getFloatingChatInstances(): string[] {
		return this.viewRegistry.getByType("floating").map((v) => v.viewId);
	}

	/**
	 * Expand a specific floating chat window by triggering a custom event.
	 * @param viewId - The viewId in "floating-chat-{id}" format (from getFloatingChatInstances())
	 */
	expandFloatingChat(viewId: string): void {
		window.dispatchEvent(
			new CustomEvent("agent-client:expand-floating-chat", {
				detail: { viewId },
			}),
		);
	}

	/**
	 * Get all available agents (claude, codex, gemini, custom)
	 */
	getAvailableAgents(): Array<{ id: string; displayName: string }> {
		return [
			{
				id: this.settings.claude.id,
				displayName:
					this.settings.claude.displayName || this.settings.claude.id,
			},
			{
				id: this.settings.codex.id,
				displayName:
					this.settings.codex.displayName || this.settings.codex.id,
			},
			{
				id: this.settings.gemini.id,
				displayName:
					this.settings.gemini.displayName || this.settings.gemini.id,
			},
			...this.settings.customAgents.map((agent) => ({
				id: agent.id,
				displayName: agent.displayName || agent.id,
			})),
		];
	}

	/**
	 * Open chat view and switch to specified agent
	 */
	private async openChatWithAgent(agentId: string): Promise<void> {
		await this.activateView();

		// Trigger new chat with specific agent
		// Pass agentId so ChatComponent knows to force new session even if empty
		this.app.workspace.trigger(
			"agent-client:new-chat-requested" as "quit",
			agentId,
		);
	}

	/**
	 * Register commands for each configured agent
	 */
	private registerAgentCommands(): void {
		const agents = this.getAvailableAgents();

		for (const agent of agents) {
			this.addCommand({
				id: `open-chat-with-${agent.id}`,
				name: `New chat with ${agent.displayName}`,
				callback: async () => {
					await this.openChatWithAgent(agent.id);
				},
			});
		}
	}

	private registerPermissionCommands(): void {
		this.addCommand({
			id: "approve-active-permission",
			name: "Approve active permission",
			callback: async () => {
				// Only activate sidebar view if the focused view is a sidebar
				// (avoid stealing focus from floating views)
				const focusedId = this.lastActiveChatViewId;
				const isFloatingFocused =
					focusedId?.startsWith("floating-chat-");
				if (!isFloatingFocused) {
					await this.activateView();
				}
				this.app.workspace.trigger(
					"agent-client:approve-active-permission" as "quit",
					this.lastActiveChatViewId,
				);
			},
		});

		this.addCommand({
			id: "reject-active-permission",
			name: "Reject active permission",
			callback: async () => {
				// Only activate sidebar view if the focused view is a sidebar
				// (avoid stealing focus from floating views)
				const focusedId = this.lastActiveChatViewId;
				const isFloatingFocused =
					focusedId?.startsWith("floating-chat-");
				if (!isFloatingFocused) {
					await this.activateView();
				}
				this.app.workspace.trigger(
					"agent-client:reject-active-permission" as "quit",
					this.lastActiveChatViewId,
				);
			},
		});

		this.addCommand({
			id: "toggle-auto-mention",
			name: "Toggle auto-mention",
			callback: async () => {
				// Only activate sidebar view if the focused view is a sidebar
				// (avoid stealing focus from floating views)
				const focusedId = this.lastActiveChatViewId;
				const isFloatingFocused =
					focusedId?.startsWith("floating-chat-");
				if (!isFloatingFocused) {
					await this.activateView();
				}
				this.app.workspace.trigger(
					"agent-client:toggle-auto-mention" as "quit",
					this.lastActiveChatViewId,
				);
			},
		});

		this.addCommand({
			id: "cancel-current-message",
			name: "Cancel current message",
			callback: () => {
				this.app.workspace.trigger(
					"agent-client:cancel-message" as "quit",
					this.lastActiveChatViewId,
				);
			},
		});

		this.addCommand({
			id: "export-chat",
			name: "Export chat",
			callback: () => {
				this.app.workspace.trigger(
					"agent-client:export-chat" as "quit",
					this.lastActiveChatViewId,
				);
			},
		});
	}

	/**
	 * Register broadcast commands for multi-view operations
	 */
	private registerBroadcastCommands(): void {
		// Broadcast prompt: Copy prompt from active view to all other views
		this.addCommand({
			id: "broadcast-prompt",
			name: "Broadcast prompt",
			callback: () => {
				this.broadcastPrompt();
			},
		});

		// Broadcast send: Send message in all views that can send
		this.addCommand({
			id: "broadcast-send",
			name: "Broadcast send",
			callback: () => {
				void this.broadcastSend();
			},
		});

		// Broadcast cancel: Cancel operation in all views
		this.addCommand({
			id: "broadcast-cancel",
			name: "Broadcast cancel",
			callback: () => {
				void this.broadcastCancel();
			},
		});
	}

	/**
	 * Copy prompt from active view to all other views
	 */
	private broadcastPrompt(): void {
		const allViews = this.viewRegistry.getAll();
		if (allViews.length === 0) {
			new Notice("[Agent Client] No chat views open");
			return;
		}

		const inputState = this.viewRegistry.toFocused((v) =>
			v.getInputState(),
		);
		if (
			!inputState ||
			(inputState.text.trim() === "" && inputState.files.length === 0)
		) {
			new Notice("[Agent Client] No prompt to broadcast");
			return;
		}

		const focusedId = this.viewRegistry.getFocusedId();
		const targetViews = allViews.filter((v) => v.viewId !== focusedId);
		if (targetViews.length === 0) {
			new Notice("[Agent Client] No other chat views to broadcast to");
			return;
		}

		for (const view of targetViews) {
			view.setInputState(inputState);
		}
	}

	/**
	 * Send message in all views that can send
	 */
	private async broadcastSend(): Promise<void> {
		const allViews = this.viewRegistry.getAll();
		if (allViews.length === 0) {
			new Notice("[Agent Client] No chat views open");
			return;
		}

		const sendableViews = allViews.filter((v) => v.canSend());
		if (sendableViews.length === 0) {
			new Notice("[Agent Client] No views ready to send");
			return;
		}

		await Promise.allSettled(sendableViews.map((v) => v.sendMessage()));
	}

	/**
	 * Cancel operation in all views
	 */
	private async broadcastCancel(): Promise<void> {
		const allViews = this.viewRegistry.getAll();
		if (allViews.length === 0) {
			new Notice("[Agent Client] No chat views open");
			return;
		}

		await Promise.allSettled(allViews.map((v) => v.cancelOperation()));
		new Notice("[Agent Client] Cancel broadcast to all views");
	}

	async loadSettings() {
		const rawSettings = ((await this.loadData()) ?? {}) as Record<
			string,
			unknown
		>;

		const claudeFromRaw =
			typeof rawSettings.claude === "object" &&
			rawSettings.claude !== null
				? (rawSettings.claude as Record<string, unknown>)
				: {};
		const codexFromRaw =
			typeof rawSettings.codex === "object" && rawSettings.codex !== null
				? (rawSettings.codex as Record<string, unknown>)
				: {};
		const geminiFromRaw =
			typeof rawSettings.gemini === "object" &&
			rawSettings.gemini !== null
				? (rawSettings.gemini as Record<string, unknown>)
				: {};

		const resolvedClaudeArgs = sanitizeArgs(claudeFromRaw.args);
		const resolvedClaudeEnv = normalizeEnvVars(claudeFromRaw.env);
		const resolvedCodexArgs = sanitizeArgs(codexFromRaw.args);
		const resolvedCodexEnv = normalizeEnvVars(codexFromRaw.env);
		const resolvedGeminiArgs = sanitizeArgs(geminiFromRaw.args);
		const resolvedGeminiEnv = normalizeEnvVars(geminiFromRaw.env);
		const customAgents = Array.isArray(rawSettings.customAgents)
			? ensureUniqueCustomAgentIds(
					rawSettings.customAgents.map((agent: unknown) => {
						const agentObj =
							typeof agent === "object" && agent !== null
								? (agent as Record<string, unknown>)
								: {};
						return normalizeCustomAgent(agentObj);
					}),
				)
			: [];

		const availableAgentIds = [
			DEFAULT_SETTINGS.claude.id,
			DEFAULT_SETTINGS.codex.id,
			DEFAULT_SETTINGS.gemini.id,
			...customAgents.map((agent) => agent.id),
		];
		// Migration: support both old activeAgentId and new defaultAgentId
		const rawDefaultId =
			typeof rawSettings.defaultAgentId === "string"
				? rawSettings.defaultAgentId.trim()
				: typeof rawSettings.activeAgentId === "string"
					? rawSettings.activeAgentId.trim()
					: "";
		const fallbackDefaultId =
			availableAgentIds.find((id) => id.length > 0) ||
			DEFAULT_SETTINGS.claude.id;
		const defaultAgentId =
			availableAgentIds.includes(rawDefaultId) && rawDefaultId.length > 0
				? rawDefaultId
				: fallbackDefaultId;

		this.settings = {
			claude: {
				id: DEFAULT_SETTINGS.claude.id,
				displayName:
					typeof claudeFromRaw.displayName === "string" &&
					claudeFromRaw.displayName.trim().length > 0
						? claudeFromRaw.displayName.trim()
						: DEFAULT_SETTINGS.claude.displayName,
				apiKey:
					typeof claudeFromRaw.apiKey === "string"
						? claudeFromRaw.apiKey
						: DEFAULT_SETTINGS.claude.apiKey,
				command:
					typeof claudeFromRaw.command === "string" &&
					claudeFromRaw.command.trim().length > 0
						? claudeFromRaw.command.trim()
						: typeof rawSettings.claudeCodeAcpCommandPath ===
									"string" &&
							  rawSettings.claudeCodeAcpCommandPath.trim()
									.length > 0
							? rawSettings.claudeCodeAcpCommandPath.trim()
							: DEFAULT_SETTINGS.claude.command,
				args: resolvedClaudeArgs.length > 0 ? resolvedClaudeArgs : [],
				env: resolvedClaudeEnv.length > 0 ? resolvedClaudeEnv : [],
			},
			codex: {
				id: DEFAULT_SETTINGS.codex.id,
				displayName:
					typeof codexFromRaw.displayName === "string" &&
					codexFromRaw.displayName.trim().length > 0
						? codexFromRaw.displayName.trim()
						: DEFAULT_SETTINGS.codex.displayName,
				apiKey:
					typeof codexFromRaw.apiKey === "string"
						? codexFromRaw.apiKey
						: DEFAULT_SETTINGS.codex.apiKey,
				command:
					typeof codexFromRaw.command === "string" &&
					codexFromRaw.command.trim().length > 0
						? codexFromRaw.command.trim()
						: DEFAULT_SETTINGS.codex.command,
				args: resolvedCodexArgs.length > 0 ? resolvedCodexArgs : [],
				env: resolvedCodexEnv.length > 0 ? resolvedCodexEnv : [],
			},
			gemini: {
				id: DEFAULT_SETTINGS.gemini.id,
				displayName:
					typeof geminiFromRaw.displayName === "string" &&
					geminiFromRaw.displayName.trim().length > 0
						? geminiFromRaw.displayName.trim()
						: DEFAULT_SETTINGS.gemini.displayName,
				apiKey:
					typeof geminiFromRaw.apiKey === "string"
						? geminiFromRaw.apiKey
						: DEFAULT_SETTINGS.gemini.apiKey,
				command:
					typeof geminiFromRaw.command === "string" &&
					geminiFromRaw.command.trim().length > 0
						? geminiFromRaw.command.trim()
						: typeof rawSettings.geminiCommandPath === "string" &&
							  rawSettings.geminiCommandPath.trim().length > 0
							? rawSettings.geminiCommandPath.trim()
							: DEFAULT_SETTINGS.gemini.command,
				args:
					resolvedGeminiArgs.length > 0
						? resolvedGeminiArgs
						: DEFAULT_SETTINGS.gemini.args,
				env: resolvedGeminiEnv.length > 0 ? resolvedGeminiEnv : [],
			},
			customAgents: customAgents,
			defaultAgentId,
			autoAllowPermissions:
				typeof rawSettings.autoAllowPermissions === "boolean"
					? rawSettings.autoAllowPermissions
					: DEFAULT_SETTINGS.autoAllowPermissions,
			autoMentionActiveNote:
				typeof rawSettings.autoMentionActiveNote === "boolean"
					? rawSettings.autoMentionActiveNote
					: DEFAULT_SETTINGS.autoMentionActiveNote,
			debugMode:
				typeof rawSettings.debugMode === "boolean"
					? rawSettings.debugMode
					: DEFAULT_SETTINGS.debugMode,
			nodePath:
				typeof rawSettings.nodePath === "string"
					? rawSettings.nodePath.trim()
					: DEFAULT_SETTINGS.nodePath,
			exportSettings: (() => {
				const rawExport = rawSettings.exportSettings as
					| Record<string, unknown>
					| null
					| undefined;
				if (rawExport && typeof rawExport === "object") {
					return {
						defaultFolder:
							typeof rawExport.defaultFolder === "string"
								? rawExport.defaultFolder
								: DEFAULT_SETTINGS.exportSettings.defaultFolder,
						filenameTemplate:
							typeof rawExport.filenameTemplate === "string"
								? rawExport.filenameTemplate
								: DEFAULT_SETTINGS.exportSettings
										.filenameTemplate,
						autoExportOnNewChat:
							typeof rawExport.autoExportOnNewChat === "boolean"
								? rawExport.autoExportOnNewChat
								: DEFAULT_SETTINGS.exportSettings
										.autoExportOnNewChat,
						autoExportOnCloseChat:
							typeof rawExport.autoExportOnCloseChat === "boolean"
								? rawExport.autoExportOnCloseChat
								: DEFAULT_SETTINGS.exportSettings
										.autoExportOnCloseChat,
						openFileAfterExport:
							typeof rawExport.openFileAfterExport === "boolean"
								? rawExport.openFileAfterExport
								: DEFAULT_SETTINGS.exportSettings
										.openFileAfterExport,
						includeImages:
							typeof rawExport.includeImages === "boolean"
								? rawExport.includeImages
								: DEFAULT_SETTINGS.exportSettings.includeImages,
						imageLocation:
							rawExport.imageLocation === "obsidian" ||
							rawExport.imageLocation === "custom" ||
							rawExport.imageLocation === "base64"
								? rawExport.imageLocation
								: DEFAULT_SETTINGS.exportSettings.imageLocation,
						imageCustomFolder:
							typeof rawExport.imageCustomFolder === "string"
								? rawExport.imageCustomFolder
								: DEFAULT_SETTINGS.exportSettings
										.imageCustomFolder,
						frontmatterTag:
							typeof rawExport.frontmatterTag === "string"
								? rawExport.frontmatterTag
								: DEFAULT_SETTINGS.exportSettings
										.frontmatterTag,
					};
				}
				return DEFAULT_SETTINGS.exportSettings;
			})(),
			windowsWslMode:
				typeof rawSettings.windowsWslMode === "boolean"
					? rawSettings.windowsWslMode
					: DEFAULT_SETTINGS.windowsWslMode,
			windowsWslDistribution:
				typeof rawSettings.windowsWslDistribution === "string"
					? rawSettings.windowsWslDistribution
					: DEFAULT_SETTINGS.windowsWslDistribution,
			sendMessageShortcut:
				rawSettings.sendMessageShortcut === "enter" ||
				rawSettings.sendMessageShortcut === "cmd-enter"
					? rawSettings.sendMessageShortcut
					: DEFAULT_SETTINGS.sendMessageShortcut,
			chatViewLocation:
				rawSettings.chatViewLocation === "right-tab" ||
				rawSettings.chatViewLocation === "right-split" ||
				rawSettings.chatViewLocation === "editor-tab" ||
				rawSettings.chatViewLocation === "editor-split"
					? rawSettings.chatViewLocation
					: DEFAULT_SETTINGS.chatViewLocation,
			displaySettings: (() => {
				const rawDisplay = rawSettings.displaySettings as
					| Record<string, unknown>
					| null
					| undefined;
				if (rawDisplay && typeof rawDisplay === "object") {
					return {
						autoCollapseDiffs:
							typeof rawDisplay.autoCollapseDiffs === "boolean"
								? rawDisplay.autoCollapseDiffs
								: DEFAULT_SETTINGS.displaySettings
										.autoCollapseDiffs,
						diffCollapseThreshold:
							typeof rawDisplay.diffCollapseThreshold ===
								"number" && rawDisplay.diffCollapseThreshold > 0
								? rawDisplay.diffCollapseThreshold
								: DEFAULT_SETTINGS.displaySettings
										.diffCollapseThreshold,
						maxNoteLength:
							typeof rawDisplay.maxNoteLength === "number" &&
							rawDisplay.maxNoteLength >= 1
								? rawDisplay.maxNoteLength
								: DEFAULT_SETTINGS.displaySettings
										.maxNoteLength,
						maxSelectionLength:
							typeof rawDisplay.maxSelectionLength === "number" &&
							rawDisplay.maxSelectionLength >= 1
								? rawDisplay.maxSelectionLength
								: DEFAULT_SETTINGS.displaySettings
										.maxSelectionLength,
						showEmojis:
							typeof rawDisplay.showEmojis === "boolean"
								? rawDisplay.showEmojis
								: DEFAULT_SETTINGS.displaySettings.showEmojis,
						fontSize: parseChatFontSize(rawDisplay.fontSize),
					};
				}
				return DEFAULT_SETTINGS.displaySettings;
			})(),
			savedSessions: Array.isArray(rawSettings.savedSessions)
				? (rawSettings.savedSessions as SavedSessionInfo[])
				: DEFAULT_SETTINGS.savedSessions,
			lastUsedModels: (() => {
				const raw = rawSettings.lastUsedModels;
				if (raw && typeof raw === "object" && !Array.isArray(raw)) {
					const result: Record<string, string> = {};
					for (const [key, value] of Object.entries(
						raw as Record<string, unknown>,
					)) {
						if (
							typeof key === "string" &&
							key.length > 0 &&
							typeof value === "string" &&
							value.length > 0
						) {
							result[key] = value;
						}
					}
					return result;
				}
				return DEFAULT_SETTINGS.lastUsedModels;
			})(),
			lastUsedModes: (() => {
				const raw = rawSettings.lastUsedModes;
				if (raw && typeof raw === "object" && !Array.isArray(raw)) {
					const result: Record<string, string> = {};
					for (const [key, value] of Object.entries(
						raw as Record<string, unknown>,
					)) {
						if (
							typeof key === "string" &&
							key.length > 0 &&
							typeof value === "string" &&
							value.length > 0
						) {
							result[key] = value;
						}
					}
					return result;
				}
				return DEFAULT_SETTINGS.lastUsedModes;
			})(),
			// Migration: support old showFloatingButton → new enableFloatingChat
			enableFloatingChat:
				typeof rawSettings.enableFloatingChat === "boolean"
					? rawSettings.enableFloatingChat
					: typeof rawSettings.showFloatingButton === "boolean"
						? rawSettings.showFloatingButton
						: DEFAULT_SETTINGS.enableFloatingChat,
			floatingButtonImage:
				typeof rawSettings.floatingButtonImage === "string"
					? rawSettings.floatingButtonImage
					: DEFAULT_SETTINGS.floatingButtonImage,
			floatingWindowSize: (() => {
				const raw = rawSettings.floatingWindowSize as
					| { width?: number; height?: number }
					| null
					| undefined;
				if (
					raw &&
					typeof raw === "object" &&
					typeof raw.width === "number" &&
					typeof raw.height === "number"
				) {
					return { width: raw.width, height: raw.height };
				}
				return DEFAULT_SETTINGS.floatingWindowSize;
			})(),
			floatingWindowPosition: (() => {
				const raw = rawSettings.floatingWindowPosition as
					| { x?: number; y?: number }
					| null
					| undefined;
				if (
					raw &&
					typeof raw === "object" &&
					typeof raw.x === "number" &&
					typeof raw.y === "number"
				) {
					return { x: raw.x, y: raw.y };
				}
				return null;
			})(),
			floatingButtonPosition: (() => {
				const raw = rawSettings.floatingButtonPosition as
					| { x?: number; y?: number }
					| null
					| undefined;
				if (
					raw &&
					typeof raw === "object" &&
					typeof raw.x === "number" &&
					typeof raw.y === "number"
				) {
					return { x: raw.x, y: raw.y };
				}
				return null;
			})(),
		};

		this.ensureDefaultAgentId();
	}

	async saveSettings() {
		await this.saveData(this.settings);
	}

	async saveSettingsAndNotify(nextSettings: AgentClientPluginSettings) {
		this.settings = nextSettings;
		await this.saveData(this.settings);
		this.settingsStore.set(this.settings);
	}

	/**
	 * Fetch the latest stable release version from GitHub.
	 */
	private async fetchLatestStable(): Promise<string | null> {
		const response = await requestUrl({
			url: "https://api.github.com/repos/RAIT-09/obsidian-agent-client/releases/latest",
		});
		const data = response.json as { tag_name?: string };
		return data.tag_name ? semver.clean(data.tag_name) : null;
	}

	/**
	 * Fetch the latest prerelease version from GitHub.
	 */
	private async fetchLatestPrerelease(): Promise<string | null> {
		const response = await requestUrl({
			url: "https://api.github.com/repos/RAIT-09/obsidian-agent-client/releases",
		});
		const releases = response.json as Array<{
			tag_name: string;
			prerelease: boolean;
		}>;

		// Find the first prerelease (releases are sorted by date descending)
		const latestPrerelease = releases.find((r) => r.prerelease);
		return latestPrerelease
			? semver.clean(latestPrerelease.tag_name)
			: null;
	}

	/**
	 * Check for plugin updates.
	 * - Stable version users: compare with latest stable release
	 * - Prerelease users: compare with both latest stable and latest prerelease
	 */
	async checkForUpdates(): Promise<boolean> {
		const currentVersion =
			semver.clean(this.manifest.version) || this.manifest.version;
		const isCurrentPrerelease = semver.prerelease(currentVersion) !== null;

		if (isCurrentPrerelease) {
			// Prerelease user: check both stable and prerelease
			const [latestStable, latestPrerelease] = await Promise.all([
				this.fetchLatestStable(),
				this.fetchLatestPrerelease(),
			]);

			const hasNewerStable =
				latestStable && semver.gt(latestStable, currentVersion);
			const hasNewerPrerelease =
				latestPrerelease && semver.gt(latestPrerelease, currentVersion);

			if (hasNewerStable || hasNewerPrerelease) {
				// Prefer stable version notification if available
				const newestVersion = hasNewerStable
					? latestStable
					: latestPrerelease;
				new Notice(
					`[Agent Client] Update available: v${newestVersion}`,
				);
				return true;
			}
		} else {
			// Stable version user: check stable only
			const latestStable = await this.fetchLatestStable();
			if (latestStable && semver.gt(latestStable, currentVersion)) {
				new Notice(`[Agent Client] Update available: v${latestStable}`);
				return true;
			}
		}

		return false;
	}

	ensureDefaultAgentId(): void {
		const availableIds = this.collectAvailableAgentIds();
		if (availableIds.length === 0) {
			this.settings.defaultAgentId = DEFAULT_SETTINGS.claude.id;
			return;
		}
		if (!availableIds.includes(this.settings.defaultAgentId)) {
			this.settings.defaultAgentId = availableIds[0];
		}
	}

	private collectAvailableAgentIds(): string[] {
		const ids = new Set<string>();
		ids.add(this.settings.claude.id);
		ids.add(this.settings.codex.id);
		ids.add(this.settings.gemini.id);
		for (const agent of this.settings.customAgents) {
			if (agent.id && agent.id.length > 0) {
				ids.add(agent.id);
			}
		}
		return Array.from(ids);
	}
}
