import { Plugin, WorkspaceLeaf, Menu, Notice, requestUrl, TFile } from 'obsidian';

import { VIEW_TYPES, SANITIZATION_CONSTANTS, UI_STRINGS } from "./constants";
import { ReleaseNotesModal } from "./modals/ReleaseNotesModal";
import { GardenerService, GardenerPlanSchema } from "./services/GardenerService";
import { GardenerStateService } from "./services/GardenerStateService";
import { GeminiProvider } from "./services/GeminiProvider";
import { GraphService } from "./services/GraphService";
import { GraphSyncOrchestrator } from "./services/GraphSyncOrchestrator";
import { McpClientManager } from "./services/McpClientManager";
import { MetadataManager } from "./services/MetadataManager";
import { LOCAL_EMBEDDING_MODELS, ModelRegistry } from "./services/ModelRegistry";
import { OntologyService } from "./services/OntologyService";
import { PersistenceManager } from "./services/PersistenceManager";
import { ProviderRegistry } from "./services/ProviderRegistry";
import { RoutingEmbeddingService } from "./services/RoutingEmbeddingService";
import { VaultManager } from "./services/VaultManager";
import { WorkerManager } from "./services/WorkerManager";
import { DEFAULT_SETTINGS, IVaultIntelligencePlugin, VaultIntelligenceSettings, VaultIntelligenceSettingTab } from "./settings";
import { IEmbeddingClient } from "./types/providers";
import { GardenerPlanRenderer } from "./ui/GardenerPlanRenderer";
import { logger } from "./utils/logger";
import { GOOGLE_API_KEY_SECRET_NAME, VOYAGE_API_KEY_SECRET_NAME, hasGoogleApiKey, hasVoyageApiKey } from "./utils/secrets";
import { ResearchChatView } from "./views/ResearchChatView";
import { SemanticGraphView } from "./views/SemanticGraphView";
import { SimilarNotesView } from "./views/SimilarNotesView";

interface InternalOllamaProvider {
	getHeaders(): Record<string, string>;
}

interface InternalSecretStorage {
	setSecret(key: string, value: string): void;
}

// Legacy prompts for migration detection (v4.2.0)
const LEGACY_SYSTEM_PROMPT = `
Role: You are an intelligent research assistant embedded within the user's Obsidian vault.
Current Date: {{DATE}}

Core Guidelines:
1. **Grounding**: You have access to the user's personal notes. Prioritize their content for questions of the type "What do I know about...".
2. **Verification**: When users ask for facts, ALWAYS verify them against real-world data using 'google_search' unless explicitly told to rely only on notes.
3. **Tool Usage**:
   - Use 'vault_search' to find notes, concepts, and connections.
   - Use 'google_search' for live news, dates, and external fact-checking.
   - Use 'computational_solver' (if available) for math, logic, and data analysis.
   - Use 'read_url' if the user provides a specific link.
4. **Context & Syntax**:
   - The user may reference specific notes using the '@' symbol (e.g., "@Note Name").
   - If the user asks "what is this?", they are referring to the currently open notes.
5. **Efficiency**: Aim to solve the user's request with as few tool calls as possible. Use parallel tool calling for independent searches. If the answer is clear, stop early.
6. **Style**: Be concise, professional, and use Markdown formatting (bolding, lists) for readability.
7. **Strict Metadata Policy**:
   - **NO FRONTMATTER**: Do NOT generate YAML frontmatter (content between --- delimiters) for any reason.
   - **Body Only**: Generate ONLY the Markdown body content. Use a single H1 header (# Title) at the top instead of metadata titles.
8. **Vault Writing Rules**:
   - **Check First**: Before creating a note, check if a similar one exists using 'vault_search'.
   - **File Extensions**: Always append .md to file paths.
   - **Safety**: Do not overwrite existing notes unless explicitly instructed to refactor them. Prefer appending.
`.trim();

const LEGACY_SYSTEM_PROMPT_v4_2_0 = `
Role: You are an intelligent research assistant embedded within the user's Obsidian vault.
Current Date: {{DATE}}

Core Guidelines:
1. **Grounding**: You have access to the user's personal notes. Prioritize their content for questions of the type "What do I know about...".
2. **Verification**: When users ask for facts, ALWAYS verify them against real-world data using 'google_search' unless explicitly told to rely only on notes.
3. **Tool Usage**:
   - Use 'vault_search' to find notes, concepts, and connections.
   - Use 'google_search' for live news, dates, and external fact-checking.
   - Use 'computational_solver' (if available) for math, logic, and data analysis.
   - Use 'read_url' if the user provides a specific link.
4. **Context & Syntax**:
   - The user may reference specific notes using the '@' symbol (e.g., "@Note Name").
   - If the user asks "what is this?", they are referring to the currently open notes.
5. **Efficiency**: Aim to solve the user's request with as few tool calls as possible. Use parallel tool calling for independent searches. If the answer is clear, stop early.
6. **Style**: Be concise, professional, and use Markdown formatting (bolding, lists) for readability.
`.trim();

const LEGACY_GARDENER_SYSTEM_PROMPT = `
You are a Gardener for an Obsidian vault. Your goal is to suggest hygiene improvements for the vault's fluid ontology (represented by the 'topics' frontmatter field).

## YOUR ROLE:
1.  **LINKING**: Identify notes missing relevant topics and suggest adding Markdown links to existing files in the 'VALID TOPICS' list below.
2.  **PROPOSING**: If you identify a recurring theme or concept that doesn't have a topic file yet, suggest a NEW topic as a Markdown link.
    - NEW topics should be placed in one of the following folders if they fit, or you can suggest a path:
{{ONTOLOGY_FOLDERS}}

## THOROUGHNESS:
- You have been provided with **{{NOTE_COUNT}}** notes in the 'NOTES' list below.
- You MUST evaluate **EVERY SINGLE NOTE** individually. 
- Do not limit yourself to a small sample; if multiple notes (or even all of them) require improvements, include them all in your 'actions' array.
- A comprehensive plan is better than a brief one. Your context window is large enough to handle many suggestions.

## CONSTRAINTS:
- Suggestions for 'topics' MUST be standard Markdown links: [Name](/Path/to/file.md).
- DO NOT use double brackets [[ ]] anywhere in the links.
- Use the EXACT vault-absolute paths provided in the 'VALID TOPICS' list below. These paths MUST start with the ontology root folder (e.g., /Ontology/...).
- **NEW TOPICS**: If you suggest a topic that is NOT in the 'VALID TOPICS' list:
    - You MUST provide a clear, concise definition for it.
    - For entities (people, organizations, places) or complex technical concepts, include at least one authoritative reference within the definition.
    - **REFERENCES**: References MUST be formatted as clickable Markdown links (e.g., [Source Name](https://...)) whenever possible. If no URL is available, provide the specific source name.
    - If suggesting multiple similar new topics (e.g. "Risk Management" vs "Enterprise Risk Management"), ensure their definitions clearly distinguish them and explain why they are separate.
    - **CRITICAL**: Check if your proposed concept is already covered by an existing topic or one of its **aliases** in the 'VALID TOPICS' list. If a semantic match exists (even if the name is slightly different), USE THE EXISTING TOPIC LINK instead of proposing a new one.
- ALWAYS provide the full updated array for 'topics'.
- DO NOT suggest changes to 'tags', 'aliases', or any other frontmatter fields. Your scope is strictly limited to the 'topics' field.
- DO NOT link to "Index" files (e.g., Concepts/Concepts.md is an index, use files *inside* it).
- DO NOT suggest removing topics unless they are clearly incorrect or typos.
- Return ONLY valid JSON.
`.trim();

/**
 * Main plugin class for Obsidian Vault Intelligence.
 * Orchestrates services for semantic search, knowledge graph maintenance,
 * and agentic reasoning (Researcher/Gardener).
 */
export default class VaultIntelligencePlugin extends Plugin implements IVaultIntelligencePlugin {
	settings!: VaultIntelligenceSettings;
	geminiService!: GeminiProvider;
	providerRegistry!: ProviderRegistry;
	embeddingService!: IEmbeddingClient;
	vaultManager!: VaultManager;
	graphService!: GraphService;
	persistenceManager!: PersistenceManager;
	metadataManager!: MetadataManager;
	ontologyService!: OntologyService;
	gardenerService!: GardenerService;
	gardenerStateService!: GardenerStateService;
	workerManager!: WorkerManager;
	graphSyncOrchestrator!: GraphSyncOrchestrator;
	mcpClientManager!: McpClientManager;
	private needsReindex = false;

	private initDebouncedHandlers() {
		// Consistently handled by VectorStore now
	}

	/**
	 * Obsidian plugin lifecycle method called when the plugin is loaded.
	 * Initializes all singleton services and registers UI components.
	 */
	async onload() {
		await this.loadSettings();

		// Check for upgrade and show release notes
		const currentVersion = this.manifest.version;
		if (this.settings.previousVersion !== currentVersion) {
			// Update setting immediately to prevent loop if fetch fails/crashes
			this.settings.previousVersion = currentVersion;
			await this.saveSettings();

			// Trigger fetch/display - fire and forget
			void (async () => {
				const sponsorUrl = await this.getSponsorUrl();
				void this.showReleaseNotes(currentVersion, sponsorUrl);
			})();
		}

		// Initialize Logger
		logger.setLevel(this.settings.logLevel);

		// 1. Initialize Base Services
		this.geminiService = new GeminiProvider(this.settings, this.app);
		this.providerRegistry = new ProviderRegistry(this.settings, this.app, this.geminiService);
		
		this.mcpClientManager = new McpClientManager(this.app, this.settings);
		void this.mcpClientManager.initialize();

		// 1b. Fetch available models asynchronously (Now uses getApiKey resolver)
		if (hasGoogleApiKey(this.settings) || hasVoyageApiKey(this.settings) || this.settings.ollamaEndpoint) {
			void (async () => {
				const apiKey = await this.geminiService.getApiKey() || ""; 
				const ollamaProvider = this.providerRegistry.getOllamaProvider() as unknown as InternalOllamaProvider;
				const ollamaHeaders = ollamaProvider?.getHeaders ? ollamaProvider.getHeaders() : {};
				// Pass skipOllamaFetch=true so we exclusively use the Ollama cache on startup to save costs
				await ModelRegistry.fetchModels(this.app, this.manifest.dir || `${this.app.vault.configDir}/plugins/vault-intelligence`, this.settings, apiKey, this.settings.modelCacheDurationDays, false, false, true, ollamaHeaders);
				// Re-sanitize after fetch completes in case dynamic limits are different
				await this.sanitizeBudgets();
			})();
		}

		// 2. Initialize Routing Embedding Provider
		this.embeddingService = new RoutingEmbeddingService(this, this.geminiService, this.settings);
		if (this.settings.embeddingProvider === 'local') {
			logger.info("Initializing Local Embedding Service");
			void (this.embeddingService as RoutingEmbeddingService).initialize().catch(err => {
				logger.error("Failed to init local worker", err);
			});
		}

		// 3. Initialize Graph Infrastructure
		this.vaultManager = new VaultManager(this.app);
		this.persistenceManager = new PersistenceManager(this);
		this.workerManager = new WorkerManager(this.app, this.embeddingService);

		this.graphService = new GraphService(this.app, this.vaultManager, this.workerManager, this.settings);

		this.metadataManager = new MetadataManager(this.app);
		this.ontologyService = new OntologyService(this.app, this.settings);
		this.gardenerStateService = new GardenerStateService(this.app, this);
		this.gardenerService = new GardenerService(this.app, this.providerRegistry, this.ontologyService, this.settings, this.gardenerStateService, this.graphService);

		// 5. Initialize GraphSyncOrchestrator (now that dependencies are ready)
		this.graphSyncOrchestrator = new GraphSyncOrchestrator(
			this.app,
			this.vaultManager,
			this.workerManager,
			this.persistenceManager,
			this.settings,
			this.ontologyService,
			this.graphService // EventBus
		);

		// 6. Late binding of providers to Facade
		this.graphService.setProviders(
			() => this.graphSyncOrchestrator.isNodeRunning,
			() => this.graphSyncOrchestrator.isScanning
		);

		// Background scan for new/changed files
		this.app.workspace.onLayoutReady(async () => {
			// Defer heavy initialization until layout is ready to unblock UI
			await this.graphSyncOrchestrator.startNode(this.needsReindex);
			await this.ontologyService.initialize();
			await this.gardenerStateService.loadState();
		});

		// Ribbon Icon
		this.addRibbonIcon(UI_STRINGS.RIBBON_ICON, UI_STRINGS.RIBBON_TOOLTIP, (evt: MouseEvent) => {
			const menu = new Menu();
			menu.addItem((item) =>
				item
					.setTitle(UI_STRINGS.EXPLORER_TITLE)
					.setIcon('layout-grid')
					.onClick(() => {
						void this.activateView(VIEW_TYPES.SIMILAR_NOTES);
					})
			);
			menu.addItem((item) =>
				item
					.setTitle(UI_STRINGS.RESEARCHER_TITLE)
					.setIcon('message-circle')
					.onClick(() => {
						void this.activateView(VIEW_TYPES.RESEARCH_CHAT);
					})
			);
			menu.addItem((item) =>
				item
					.setTitle(UI_STRINGS.SEMANTIC_GRAPH_TITLE)
					.setIcon('network')
					.onClick(() => {
						void this.activateView(VIEW_TYPES.SEMANTIC_GRAPH);
					})
			);
			menu.showAtMouseEvent(evt);
		});

		// Register Views
		this.registerView(
			VIEW_TYPES.RESEARCH_CHAT,
			(leaf) => new ResearchChatView(leaf, this, this.providerRegistry, this.graphService, this.embeddingService)
		);
		this.registerView(
			VIEW_TYPES.SEMANTIC_GRAPH,
			(leaf) => new SemanticGraphView(leaf, this, this.graphService)
		);
		this.registerView(
			VIEW_TYPES.SIMILAR_NOTES,
			(leaf) => new SimilarNotesView(leaf, this, this.graphService)
		);

		// Commands
		this.addCommand({
			callback: async () => {
				try {
					await this.gardenerService.purgeOldPlans();
					new Notice(UI_STRINGS.NOTICE_GARDENER_PURGED);
				} catch (error: unknown) {
					new Notice(`${UI_STRINGS.NOTICE_PURGE_FAILED}${error instanceof Error ? error.message : String(error)}`);
				}
			},
			id: 'gardener-purge-plans',
			name: UI_STRINGS.GARDENER_TITLE_PURGE
		});

		this.addCommand({
			callback: async () => {
				try {
					const planFile = await this.gardenerService.tidyVault();
					if (planFile) {
						const leaf = this.app.workspace.getLeaf('tab');
						await leaf.openFile(planFile);
					}
				} catch (error: unknown) {
					const message = error instanceof Error ? error.message : String(error);
					new Notice(`${UI_STRINGS.NOTICE_GARDENER_FAILED}${message}`);
				}
			},
			id: 'gardener-tidy-vault',
			name: UI_STRINGS.GARDENER_TITLE_TIDY
		});

		this.addCommand({
			callback: async () => {
				try {
					const planFile = await this.gardenerService.tidyVault(true);
					if (planFile) {
						const leaf = this.app.workspace.getLeaf('tab');
						await leaf.openFile(planFile);
					}
				} catch (error: unknown) {
					const message = error instanceof Error ? error.message : String(error);
					new Notice(`${UI_STRINGS.NOTICE_GARDENER_FAILED}${message}`);
				}
			},
			id: 'gardener-tidy-vault-cost-optimizer',
			name: 'Gardener: organize vault concepts (new files only)'
		});

		this.addCommand({
			callback: () => {
				void this.activateView(VIEW_TYPES.RESEARCH_CHAT);
			},
			id: 'open-research-chat-view',
			name: 'Researcher: chat with vault'
		});

		this.addCommand({
			callback: () => {
				void this.activateView(VIEW_TYPES.SEMANTIC_GRAPH);
			},
			id: 'open-semantic-graph-view',
			name: 'Explorer: view semantic galaxy'
		});

		this.addCommand({
			callback: () => {
				void this.activateView(VIEW_TYPES.SIMILAR_NOTES);
			},
			id: 'open-similar-notes-view',
			name: 'Explorer: view similar notes'
		});

		this.addCommand({
			callback: async () => {
				const sponsorUrl = await this.getSponsorUrl();
				void this.showReleaseNotes(this.manifest.version, sponsorUrl);
			},
			id: 'show-release-notes',
			name: `Show release notes`
		});

		// Markdown Post Processors
		this.registerMarkdownCodeBlockProcessor("gardener-plan", (source, el, ctx) => {
			try {
				const rawPlan = JSON.parse(source) as unknown;
				const plan = GardenerPlanSchema.safeParse(rawPlan);

				if (plan.success) {
					const renderer = new GardenerPlanRenderer(this.app, el, plan.data, this.metadataManager, this.ontologyService, this.gardenerStateService, this.settings);
					ctx.addChild(renderer);
				} else {
					el.createEl("pre", { cls: "gardener-error", text: `Invalid Gardener Plan schema: ${plan.error.message}` });
				}
			} catch (error: unknown) {
				const message = error instanceof Error ? error.message : String(error);
				logger.error("Failed to render Gardener Plan", message);
				el.createEl("pre", { cls: "gardener-error", text: `Error parsing Gardener Plan: ${message}` });
			}
		});

		// Settings Tab
		this.addSettingTab(new VaultIntelligenceSettingTab(this.app, this));

		// --- Global Event Listeners ---
		this.registerEvent(
			this.app.workspace.on('active-leaf-change', () => {
				// 1. Refresh Similar Notes (Explorer)
				const leaves = this.app.workspace.getLeavesOfType(VIEW_TYPES.SIMILAR_NOTES);
				leaves.forEach(leaf => {
					if (leaf.view instanceof SimilarNotesView) {
						void leaf.view.updateView();
					}
				});

				// 2. Refresh Semantic Galaxy
				const graphLeaves = this.app.workspace.getLeavesOfType(VIEW_TYPES.SEMANTIC_GRAPH);
				graphLeaves.forEach(leaf => {
					if (leaf.view instanceof SemanticGraphView) {
						const file = this.app.workspace.getActiveFile();
						void leaf.view.updateForFile(file);
					}
				});
			})
		);

		logger.info(UI_STRINGS.NOTICE_PLUGIN_LOADED);
	}

	/**
	 * Obsidian plugin lifecycle method called when the plugin is unloaded.
	 * Ensures clean shutdown of workers and saves final state.
	 */
	onunload() {
		if (this.mcpClientManager) {
			this.mcpClientManager.terminate().catch((e: unknown) => {
				logger.error("Error terminating MCP Client Manager", e);
			});
		}

		if (this.graphSyncOrchestrator) {
			this.graphSyncOrchestrator.flushAndShutdown().catch((e: unknown) => {
				logger.error("Error during graph shutdown", e);
			});
		}
		// Terminate
		if (this.embeddingService instanceof RoutingEmbeddingService) {
			this.embeddingService.terminate().catch((e: unknown) => {
				logger.error("Error during embedding service shutdown", e);
			});
		}

		logger.info(UI_STRINGS.NOTICE_PLUGIN_UNLOADED);
	}

	async loadSettings() {
		const data = await this.loadData() as Partial<VaultIntelligenceSettings> | null;
		this.settings = Object.assign({}, DEFAULT_SETTINGS, data);

		// Migration: Decouple gardener exclusions (v8.3.0)
		if (data && data.excludedFolders && !data.gardenerExcludedFolders) {
			this.settings.gardenerExcludedFolders = [...data.excludedFolders];
			logger.info("[Migration] Migrated excludedFolders to gardenerExcludedFolders");
		}

		// Flag for auto-reindex if version is older than latest (v2 introduced field separation)
		const loadedVersion = data?.indexVersion ?? 1;
		if (loadedVersion < DEFAULT_SETTINGS.indexVersion) {
			logger.warn(`[Index Migration] Stale index version detected (${loadedVersion}). Forcing re-index.`);
			this.needsReindex = true;
			this.settings.indexVersion = DEFAULT_SETTINGS.indexVersion;
			await this.saveSettings();
		}

		// Migration: Convert Legacy Default Prompts to Reference Mode (null)
		if (this.settings.systemInstruction === LEGACY_SYSTEM_PROMPT || this.settings.systemInstruction === LEGACY_SYSTEM_PROMPT_v4_2_0) {
			logger.info("Migrating legacy system prompt to default-by-reference (null)");
			this.settings.systemInstruction = null;
		}

		if (this.settings.gardenerSystemInstruction === LEGACY_GARDENER_SYSTEM_PROMPT) {
			logger.info("Migrating legacy gardener prompt to default-by-reference (null)");
			this.settings.gardenerSystemInstruction = null;
		}

		// Migration: Preserve secret key references separately from raw API keys.
		if (this.settings.googleApiKey && !this.settings.googleApiKey.startsWith('AIza') && !this.settings.googleApiKeySecret) {
			logger.info("Migrating legacy Google API key reference into googleApiKeySecret.");
			this.settings.googleApiKeySecret = this.settings.googleApiKey;
			this.settings.googleApiKey = '';
			await this.saveSettings();
		}

		// --- Tiered Context Control Migration (v8.2.0) ---
		// Migrate legacy local context budgets to explicitly overridden model budgets
		// Ensures users who heavily customized their local VRAM budget don't lose it on upgrade.
		if ((this.settings.chatModel?.includes('ollama') || this.settings.chatModel?.includes('local') || this.settings.embeddingProvider === 'local' || this.settings.embeddingProvider === 'ollama') && this.settings.contextWindowTokens !== DEFAULT_SETTINGS.contextWindowTokens) {
			if (this.settings.chatModel && typeof this.settings.contextWindowTokens === 'number') {
				this.settings.modelContextOverrides[this.settings.chatModel] = this.settings.contextWindowTokens;
				this.settings.contextWindowTokens = DEFAULT_SETTINGS.contextWindowTokens;
				logger.info(`[Migration] Rescued local chat context budget into modelContextOverrides for ${this.settings.chatModel}`);
			}
		}

		if ((this.settings.gardenerModel?.includes('ollama') || this.settings.gardenerModel?.includes('local')) && this.settings.gardenerContextBudget !== DEFAULT_SETTINGS.gardenerContextBudget) {
			if (this.settings.gardenerModel && typeof this.settings.gardenerContextBudget === 'number') {
				this.settings.modelContextOverrides[this.settings.gardenerModel] = this.settings.gardenerContextBudget;
				this.settings.gardenerContextBudget = DEFAULT_SETTINGS.gardenerContextBudget;
				logger.info(`[Migration] Rescued local gardener context budget into modelContextOverrides for ${this.settings.gardenerModel}`);
			}
		}

		// Migration: Rename gardenerRecheckHours to gardenerRecheckDays (v8.1.0)
		const rawData = data as Record<string, unknown>;
		if (rawData && typeof rawData.gardenerRecheckHours === 'number') {
			const hours = rawData.gardenerRecheckHours;
			// Convert to days and round to 3 decimal places to avoid float jitter
			this.settings.gardenerRecheckDays = Math.round((hours / 24) * 1000) / 1000;
			delete rawData.gardenerRecheckHours;
			delete (this.settings as unknown as Record<string, unknown>)['gardenerRecheckHours'];
			await this.saveSettings();
			logger.info(`Migrating gardenerRecheckHours (${hours}) to gardenerRecheckDays (${this.settings.gardenerRecheckDays})`);
		}

		// --- Voyage SecretStorage Migration ---
		if (this.settings.voyageApiKey && (this.settings.voyageApiKey.startsWith('pa-') || this.settings.voyageApiKey.startsWith('al-')) && !this.settings.secretStorageFailure) {
			try {
				const storage = this.app.secretStorage as unknown as InternalSecretStorage | undefined;
				if (storage && storage.setSecret) {
					storage.setSecret(VOYAGE_API_KEY_SECRET_NAME, this.settings.voyageApiKey);
					this.settings.voyageApiKeySecret = VOYAGE_API_KEY_SECRET_NAME;
					this.settings.voyageApiKey = '';
					await this.saveData(this.settings);
				}
			} catch (error) {
				logger.error("[SecretStorage] Voyage migration failed:", error);
			}
		}

		// --- SecretStorage Migration (v7.0.0) ---
		if (this.settings.googleApiKey && this.settings.googleApiKey.startsWith('AIza') && !this.settings.secretStorageFailure) {
			try {
				logger.info("[SecretStorage] Migrating API key to secure storage...");
				// SecretStorage is synchronous in v1.11.4+
				const storage = this.app.secretStorage as unknown as InternalSecretStorage | undefined;

				if (storage && storage.setSecret) {
					storage.setSecret(GOOGLE_API_KEY_SECRET_NAME, this.settings.googleApiKey);
					this.settings.googleApiKeySecret = GOOGLE_API_KEY_SECRET_NAME;
					this.settings.googleApiKey = '';
					await this.saveData(this.settings);
					logger.info("[SecretStorage] Migration successful.");
				} else {
					throw new Error("SecretStorage API not available.");
				}
			} catch (error) {
				logger.error("[SecretStorage] Migration failed:", error);
				// Suppress future attempts to avoid "Nag Loop" on broken Linux systems
				this.settings.secretStorageFailure = true;
				await this.saveData(this.settings);
				new Notice("Secure storage unavailable. Storing API key as plain text.");
			}
		}

		// --- MCP Plaintext Secrets Migration (v8.x.x) ---
		let mcpSecretsMigrated = false;
		if (this.settings.mcpServers) {
			const storage = this.app.secretStorage as unknown as InternalSecretStorage | undefined;
			if (storage && storage.setSecret && !this.settings.secretStorageFailure) {
				for (const server of this.settings.mcpServers) {
					// Migrate Environment Variables
					if (server.env && server.env.trim()) {
						try {
							const parsed = JSON.parse(server.env) as Record<string, string>;
							let modified = false;
							for (const [k, v] of Object.entries(parsed)) {
								// Basic heuristic: if it looks like a token/key and not already a secret pointer
								if (!v.startsWith('vi-secret:') && (k.toLowerCase().includes('key') || k.toLowerCase().includes('token') || k.toLowerCase().includes('secret') || k.toLowerCase().includes('password'))) {
									const secretKey = `mcp-${server.id}-env-${k}`;
									storage.setSecret(secretKey, v);
									parsed[k] = `vi-secret:${secretKey}`;
									modified = true;
								}
							}
							if (modified) {
								server.env = JSON.stringify(parsed);
								mcpSecretsMigrated = true;
							}
						} catch (e) {
							logger.warn(`Migration: Failed to parse env for MCP server ${server.name}`, e);
						}
					}

					// Migrate HTTP Headers
					if (server.remoteHeaders && server.remoteHeaders.trim()) {
						try {
							const parsed = JSON.parse(server.remoteHeaders) as Record<string, string>;
							let modified = false;
							for (const [k, v] of Object.entries(parsed)) {
								if (!v.startsWith('vi-secret:') && (k.toLowerCase().includes('authorization') || k.toLowerCase().includes('token') || k.toLowerCase().includes('key') || k.toLowerCase().includes('api'))) {
									const secretKey = `mcp-${server.id}-headers-${k}`;
									storage.setSecret(secretKey, v);
									parsed[k] = `vi-secret:${secretKey}`;
									modified = true;
								}
							}
							if (modified) {
								server.remoteHeaders = JSON.stringify(parsed);
								mcpSecretsMigrated = true;
							}
						} catch (e) {
							logger.warn(`Migration: Failed to parse remoteHeaders for MCP server ${server.name}`, e);
						}
					}
				}
			}
		}

		if (mcpSecretsMigrated) {
			logger.info("[SecretStorage] Migrated plaintext MCP secrets to secure storage.");
			// We save it immediately
			await this.saveData(this.settings);
		}

		await this.sanitizeBudgets();

		// --- Model ID Prefix Migration (v8.1.0) ---
		let prefixChanged = false;
		if (this.settings.embeddingProvider === 'local' && !this.settings.embeddingModel.startsWith('local/')) {
			this.settings.embeddingModel = `local/${this.settings.embeddingModel}`;
			prefixChanged = true;
		} else if (this.settings.embeddingProvider === 'ollama' && !this.settings.embeddingModel.startsWith('ollama/')) {
			this.settings.embeddingModel = `ollama/${this.settings.embeddingModel}`;
			prefixChanged = true;
		}

		if (prefixChanged) {
			logger.info(`[Migration] Updated model ID with prefix: ${this.settings.embeddingModel}`);
			await this.saveData(this.settings);
		}

		// Sanity check: Ensure dimensions match presets if using a local provider
		if (this.settings.embeddingModel.startsWith('local/')) {
			const modelId = this.settings.embeddingModel;
			const modelDef = LOCAL_EMBEDDING_MODELS.find(m => m.id === modelId);

			if (modelDef?.dimensions && this.settings.embeddingDimension !== modelDef.dimensions) {
				logger.warn(`Fixing stale dimension for ${modelDef.label}: ${this.settings.embeddingDimension} -> ${modelDef.dimensions}`);
				this.settings.embeddingDimension = modelDef.dimensions;
				await this.saveData(this.settings);
			}

			// Migration: v1.5 -> v1 (v1.5 seems to be broken/unavailable in Xenova repo)
			if (modelId === 'local/Xenova/nomic-embed-text-v1.5') {
				const nomicV1 = 'local/Xenova/nomic-embed-text-v1';
				logger.info(`Migrating model from v1.5 to v1: ${modelId} -> ${nomicV1}`);
				this.settings.embeddingModel = nomicV1;
				this.settings.embeddingDimension = SANITIZATION_CONSTANTS.DEFAULT_EMBEDDING_DIMENSION;
				await this.saveData(this.settings);
			}
		}
	}

	async saveSettings(requiresWorkerRestart = false) {
		await this.saveData(this.settings);
		if (logger) logger.setLevel(this.settings.logLevel);
		if (this.geminiService) this.geminiService.updateSettings(this.settings);
		if (this.mcpClientManager) void this.mcpClientManager.updateSettings(this.settings);
		if (this.embeddingService && (this.embeddingService as RoutingEmbeddingService).updateConfiguration) {
			(this.embeddingService as RoutingEmbeddingService).updateConfiguration();
		}

		if (this.graphSyncOrchestrator) {
			if (requiresWorkerRestart) {
				await this.graphSyncOrchestrator.commitConfigChange();
			} else {
				await this.graphSyncOrchestrator.updateConfig(this.settings);
			}
		}
	}

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

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

		if (leaves.length > 0) {
			leaf = leaves[0] ?? null;
		} else {
			const rightLeaf = workspace.getRightLeaf(false);
			if (rightLeaf) {
				leaf = rightLeaf;
				await leaf.setViewState({ active: true, type: viewType });
			}
		}

		if (leaf) void workspace.revealLeaf(leaf);
	}

	/**
	 * Ensures context budgets are within safe safe integer bounds and capped at model limits.
	 * Corrects "silly" values that might be present in data.json.
	 */
	async sanitizeBudgets() {
		let changed = false;

		const sanitize = (val: number, modelId: string, defaultVal: number): number => {
			const limit = ModelRegistry.getModelById(modelId)?.inputTokenLimit ?? SANITIZATION_CONSTANTS.MAX_TOKEN_LIMIT_SANITY;

			// 1. Initial sanity: must be a safe integer
			let cleaned = Number.isSafeInteger(val) ? val : defaultVal;

			// 2. Cap at model limit
			if (cleaned > limit) {
				cleaned = limit;
				changed = true;
			}

			// 3. Floor for usability
			if (cleaned < SANITIZATION_CONSTANTS.MIN_TOKEN_LIMIT) {
				cleaned = SANITIZATION_CONSTANTS.MIN_TOKEN_LIMIT;
				changed = true;
			}

			if (cleaned !== val) changed = true;
			return cleaned;
		};

		this.settings.contextWindowTokens = sanitize(
			this.settings.contextWindowTokens,
			this.settings.chatModel,
			DEFAULT_SETTINGS.contextWindowTokens
		);

		this.settings.gardenerContextBudget = sanitize(
			this.settings.gardenerContextBudget,
			this.settings.gardenerModel,
			DEFAULT_SETTINGS.gardenerContextBudget
		);

		// Sanitize all explicitly defined model overrides
		if (this.settings.modelContextOverrides) {
			for (const [modelId, overrideValue] of Object.entries(this.settings.modelContextOverrides)) {
				// Determine the model's actual native limit if known, otherwise fallback to global max sanity
				const modelNativeLimit = ModelRegistry.getModelById(modelId)?.inputTokenLimit ?? SANITIZATION_CONSTANTS.MAX_TOKEN_LIMIT_SANITY;
				let cleanedOverride = Number.isSafeInteger(overrideValue) ? overrideValue : SANITIZATION_CONSTANTS.DEFAULT_LOCAL_CONTEXT_TOKENS;

				if (cleanedOverride > modelNativeLimit) {
					cleanedOverride = modelNativeLimit;
					changed = true;
				}

				if (cleanedOverride < SANITIZATION_CONSTANTS.MIN_TOKEN_LIMIT) {
					cleanedOverride = SANITIZATION_CONSTANTS.MIN_TOKEN_LIMIT;
					changed = true;
				}

				if (cleanedOverride !== overrideValue) {
					this.settings.modelContextOverrides[modelId] = cleanedOverride;
					changed = true;
				}
			}
		}

		if (changed) {
			logger.info(UI_STRINGS.NOTICE_SANITISED_BUDGETS);
			await this.saveData(this.settings);
		}

		// Migration: Bump voyageRetries to match geminiRetries for existing users (v9.3.8)
		if (this.settings.voyageRetries === 3) {
			logger.info("[Migration] Bumping voyageRetries from 3 to 10 for improved resilience.");
			this.settings.voyageRetries = 10;
			await this.saveSettings();
		}
	}

	async showReleaseNotes(version: string, sponsorUrl?: string) {
		const repo = "cybaea/obsidian-vault-intelligence";
		const apiUrl = `https://api.github.com/repos/${repo}/releases/tags/${version}`;
		const webUrl = `https://github.com/${repo}/releases/tag/${version}`;

		try {
			const response = await requestUrl({ url: apiUrl });

			if (response.status === 200) {
				const data = response.json as { body: string };
				if (data.body) {
					new ReleaseNotesModal(this.app, this, version, data.body, sponsorUrl).open();
				} else {
					throw new Error("Release body empty or not found");
				}
			} else {
				throw new Error("Release body empty or not found");
			}
		} catch (error) {
			logger.error(`Failed to fetch release notes for v${version}`, error);

			// Fallback content with string formatting
			const errorTitle = UI_STRINGS.MODAL_RELEASE_NOTES_ERROR_HEADER;
			const errorBody = UI_STRINGS.MODAL_RELEASE_NOTES_ERROR_BODY
				.replace("{0}", version)
				.replace("{1}", webUrl);

			const fallbackMarkdown = `${errorTitle}\n\n${errorBody}`;

			new ReleaseNotesModal(this.app, this, version, fallbackMarkdown, sponsorUrl).open();
		}
	}

	private async getSponsorUrl(): Promise<string | undefined> {
		try {
			const fundingFile = ".github/FUNDING.yml";
			const file = this.app.vault.getAbstractFileByPath(fundingFile);
			if (file instanceof TFile) {
				const content = await this.app.vault.read(file);
				const githubLine = content.split("\n").find(line => line.trim().startsWith("github:"));
				if (githubLine) {
					const parts = githubLine.split(":");
					const user = parts[1]?.trim();
					if (user) {
						return `https://github.com/sponsors/${user}`;
					}
				}
			}
		} catch (error) {
			logger.warn("Failed to read FUNDING.yml dynamically", error);
		}
		return undefined;
	}
}