import { pathBasename, pathStartsWith } from "../utils/pathUtils";
import { currentBranchKey, repositoriesStore } from "./repositories";
import { type BaseTab, createTabManager } from "./tabManager";

// Zoom bounds mirror the terminal zoom (useTerminalLifecycle) for consistency.
const MD_MIN_FONT_SIZE = 8;
const MD_MAX_FONT_SIZE = 32;
const MD_FONT_STEP = 2;
/** Default matches #markdown-content font-size: var(--font-lg) = 15px in styles.css */
export const MD_DEFAULT_FONT_SIZE = 15;

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

/** File-based markdown tab (reads content from the filesystem via Tauri) */
export interface FileTab extends BaseTab {
	type: "file";
	repoPath: string;
	filePath: string;
	/** Display name (basename of filePath) */
	fileName: string;
	/** Effective filesystem root for I/O (worktree path when on a linked worktree, otherwise same as repoPath) */
	fsRoot?: string;
}

/** Virtual markdown tab (content generated by a plugin via markdownProviderRegistry) */
export interface VirtualTab extends BaseTab {
	type: "virtual";
	title: string;
	/** URI routed through markdownProviderRegistry, e.g. "plan:file?path=/foo.md" */
	contentUri: string;
}

/** Plugin panel tab (renders HTML in a sandboxed iframe via PluginPanel component) */
export interface PluginPanelTab extends BaseTab {
	type: "plugin-panel";
	title: string;
	/** The plugin that owns this panel */
	pluginId: string;
	/** HTML content rendered inside the sandboxed iframe */
	html: string;
	/** Optional URL to load instead of inline HTML (mutually exclusive with html) */
	url?: string;
}

/** Native Claude Usage Dashboard tab */
export interface ClaudeUsageTab extends BaseTab {
	type: "claude-usage";
	title: string;
}

/** PR diff tab (renders a full PR diff in the main panel) */
export interface PrDiffTab extends BaseTab {
	type: "pr-diff";
	title: string;
	repoPath: string;
	prNumber: number;
	prTitle: string;
	/** Raw unified diff content */
	diff: string;
}

/** HTML preview tab (renders HTML in a sandboxed iframe) */
export interface HtmlPreviewTab extends BaseTab {
	type: "html-preview";
	title: string;
	repoPath: string;
	filePath: string;
	fileName: string;
	fsRoot?: string;
}

/** Native Command Overview panel tab */
export interface CommandOverviewTab extends BaseTab {
	type: "command-overview";
	title: string;
}

/** Discriminated union of all markdown tab types */
export type MdTabData =
	| FileTab
	| VirtualTab
	| PluginPanelTab
	| ClaudeUsageTab
	| PrDiffTab
	| HtmlPreviewTab
	| CommandOverviewTab;

/**
 * Resolve a caller-supplied path (repo root or PTY cwd) to a registered repo
 * path. Exact match first, then longest-prefix match so that nested cwds
 * (e.g. `/repo/src/foo`) still map back to the repo root (`/repo`).
 * Returns null when no registered repo owns the path.
 */
export function resolveRepoForCwd(cwd: string | null | undefined): string | null {
	if (!cwd) return null;
	const repos = Object.keys(repositoriesStore.state.repositories);
	if (repos.includes(cwd)) return cwd;
	let best: string | null = null;
	for (const repo of repos) {
		if (pathStartsWith(cwd, repo) && (best === null || repo.length > best.length)) {
			best = repo;
		}
	}
	return best;
}

// ---------------------------------------------------------------------------
// Store
// ---------------------------------------------------------------------------

/** Imperative handles exposed by tab components (e.g. MarkdownTabHandle) */
const handles = new Map<string, unknown>();

function createMdTabsStore() {
	const base = createTabManager<MdTabData>("markdown");

	return {
		state: base.state,
		remove: base.remove,
		setActive: base.setActive,
		clearAll: base.clearAll,
		get: base.get,
		getIds: base.getIds,
		getVisibleIds: base.getVisibleIds,
		getActive: base.getActive,
		getCount: base.getCount,
		setPinned: base.setPinned,
		reorderByIds: base.reorderByIds,

		/** Add a file-based markdown tab (or return existing if same file already open).
		 *  `fsRoot` overrides the filesystem root for I/O (e.g. worktree path). */
		add(repoPath: string, filePath: string, fsRoot?: string): string {
			const effectiveRoot = fsRoot || repoPath;
			const existing = Object.values(base.state.tabs).find(
				(tab) => tab.type === "file" && (tab as FileTab).fsRoot === effectiveRoot && tab.filePath === filePath,
			) as FileTab | undefined;
			if (existing) {
				const key = currentBranchKey();
				if (existing.repoPath !== repoPath || existing.branchKey !== key) {
					base._setState("tabs", existing.id, "repoPath" as keyof MdTabData, repoPath as MdTabData[keyof MdTabData]);
					base._setState("tabs", existing.id, "branchKey" as keyof MdTabData, key as MdTabData[keyof MdTabData]);
				}
				base.setActive(existing.id);

				return existing.id;
			}

			const id = base._nextId("md");
			const fileName = pathBasename(filePath) || filePath;
			const tab: FileTab = {
				type: "file",
				id,
				repoPath,
				filePath,
				fileName,
				branchKey: currentBranchKey(),
				fsRoot: effectiveRoot,
			};
			const tabId = base._addTab(tab);

			return tabId;
		},

		/** Add a file-based markdown tab in the background (does not change activeId).
		 *  Returns tab ID, or null if a tab for the same file is already open. */
		addFileBackground(repoPath: string, filePath: string, fsRoot?: string): string | null {
			const effectiveRoot = fsRoot || repoPath;
			const existing = Object.values(base.state.tabs).find(
				(tab) => tab.type === "file" && (tab as FileTab).fsRoot === effectiveRoot && tab.filePath === filePath,
			) as FileTab | undefined;
			if (existing) return null;

			const id = base._nextId("md");
			const fileName = pathBasename(filePath) || filePath;
			const tab: FileTab = {
				type: "file",
				id,
				repoPath,
				filePath,
				fileName,
				branchKey: currentBranchKey(),
				fsRoot: effectiveRoot,
				pinned: true,
			};
			base._addTabBackground(tab);
			return id;
		},

		/** Add a virtual markdown tab (or return existing if same contentUri already open) */
		addVirtual(title: string, contentUri: string, repoPath?: string): string {
			const existing = Object.values(base.state.tabs).find(
				(tab) => tab.type === "virtual" && tab.contentUri === contentUri,
			) as VirtualTab | undefined;
			if (existing) {
				base.setActive(existing.id);

				return existing.id;
			}

			const id = base._nextId("md");
			const tab: VirtualTab = { type: "virtual", id, title, contentUri, pinned: true };
			if (repoPath) tab.repoPath = repoPath;
			const tabId = base._addTab(tab);

			return tabId;
		},

		/** Add a virtual markdown tab without making it active (background tab). Returns tab ID or null if already open. */
		addVirtualBackground(title: string, contentUri: string, repoPath?: string): string | null {
			const existing = Object.values(base.state.tabs).find(
				(tab) => tab.type === "virtual" && tab.contentUri === contentUri,
			) as VirtualTab | undefined;
			if (existing) return null; // already open — no-op

			const id = base._nextId("md");
			// Add to store without changing activeId
			const tab: VirtualTab = { type: "virtual", id, title, contentUri, pinned: true };
			if (repoPath) tab.repoPath = repoPath;
			base._addTabBackground(tab);
			return id;
		},

		/**
		 * Add a plugin panel tab (or return existing if same pluginId+title already open).
		 * Returns the tab ID.
		 */
		addPluginPanel(pluginId: string, title: string, html: string): string {
			const existing = Object.values(base.state.tabs).find(
				(tab) => tab.type === "plugin-panel" && (tab as PluginPanelTab).pluginId === pluginId && tab.title === title,
			) as PluginPanelTab | undefined;
			if (existing) {
				// Refresh html so plugin updates (e.g. after hot-reload or
				// re-open) replace stale content instead of silently keeping
				// the previous render.
				base._setState("tabs", existing.id, "html" as keyof MdTabData, html as MdTabData[keyof MdTabData]);
				return existing.id;
			}

			const id = base._nextId("md");
			const tabId = base._addTab({ type: "plugin-panel", id, title, pluginId, html, pinned: true } as PluginPanelTab);

			return tabId;
		},

		/**
		 * Open or update a UI tab (MCP-driven). Deduplicates on pluginId alone.
		 * focus=true (default): switch to this tab. focus=false: update content silently.
		 *
		 * originRepoPath: repo/cwd of the MCP caller. When resolvable to a
		 * registered repo, the tab is scoped there. When unresolvable, the tab
		 * is left unscoped (visible in all repos) rather than guessing from focus.
		 */
		openUiTab(
			pluginId: string,
			title: string,
			html: string,
			pinned: boolean,
			url?: string,
			focus = true,
			originRepoPath?: string,
		): string {
			const existing = Object.values(base.state.tabs).find(
				(tab) => tab.type === "plugin-panel" && (tab as PluginPanelTab).pluginId === pluginId,
			) as PluginPanelTab | undefined;
			if (existing) {
				base._setState("tabs", existing.id, "html" as keyof MdTabData, html as MdTabData[keyof MdTabData]);
				base._setState("tabs", existing.id, "title" as keyof MdTabData, title as MdTabData[keyof MdTabData]);
				if (url !== undefined)
					base._setState("tabs", existing.id, "url" as keyof MdTabData, url as MdTabData[keyof MdTabData]);
				if (focus) base.setActive(existing.id);
				return existing.id;
			}

			const id = base._nextId("md");
			const tab: PluginPanelTab = { type: "plugin-panel", id, title, pluginId, html, pinned };
			const resolvedRepo = resolveRepoForCwd(originRepoPath) ?? undefined;
			if (resolvedRepo) tab.repoPath = resolvedRepo;
			if (url) tab.url = url;
			if (focus) {
				return base._addTab(tab);
			}
			base._addTabBackground(tab);
			return id;
		},

		/** Remove a plugin-panel tab by its pluginId (the MCP 'id' param). No-op if not found. */
		closeUiTab(pluginId: string): void {
			const existing = Object.values(base.state.tabs).find(
				(tab) => tab.type === "plugin-panel" && (tab as PluginPanelTab).pluginId === pluginId,
			) as PluginPanelTab | undefined;
			if (existing) base.remove(existing.id);
		},

		/** Update the HTML content of an existing plugin panel tab */
		updatePluginPanel(tabId: string, html: string): void {
			const tab = base.get(tabId);
			if (tab && tab.type === "plugin-panel") {
				base._setState("tabs", tabId, "html" as keyof MdTabData, html as MdTabData[keyof MdTabData]);
			}
		},

		/** Add the Claude Usage Dashboard tab (singleton — reuses existing if open) */
		addClaudeUsage(): string {
			const existing = Object.values(base.state.tabs).find((tab) => tab.type === "claude-usage") as
				| ClaudeUsageTab
				| undefined;
			if (existing) {
				base.setActive(existing.id);

				return existing.id;
			}

			const id = base._nextId("md");
			const tabId = base._addTab({ type: "claude-usage", id, title: "Claude Usage", pinned: true } as ClaudeUsageTab);

			return tabId;
		},

		/** Add the Command Overview panel tab (singleton — reuses existing if open) */
		addCommandOverview(): string {
			const existing = Object.values(base.state.tabs).find((tab) => tab.type === "command-overview") as
				| CommandOverviewTab
				| undefined;
			if (existing) {
				base.setActive(existing.id);
				return existing.id;
			}

			const id = base._nextId("md");
			return base._addTab({ type: "command-overview", id, title: "Commands", pinned: true } as CommandOverviewTab);
		},

		/** Add the AI Triage panel tab (singleton per repo — reuses existing if open) */
		/** Add a PR diff tab (or reuse existing for same repo+prNumber, updating diff content) */
		addPrDiff(repoPath: string, prNumber: number, prTitle: string, diff: string): string {
			const existing = Object.values(base.state.tabs).find(
				(tab) =>
					tab.type === "pr-diff" &&
					(tab as PrDiffTab).repoPath === repoPath &&
					(tab as PrDiffTab).prNumber === prNumber,
			) as PrDiffTab | undefined;
			if (existing) {
				// Update diff content in case it changed
				base._setState("tabs", existing.id, "diff" as keyof MdTabData, diff as MdTabData[keyof MdTabData]);
				base.setActive(existing.id);
				return existing.id;
			}

			const id = base._nextId("md");
			const title = `PR #${prNumber}`;
			const tabId = base._addTab({
				type: "pr-diff",
				id,
				title,
				repoPath,
				prNumber,
				prTitle,
				diff,
				pinned: false,
			} as PrDiffTab);
			return tabId;
		},

		/** Add an HTML preview tab (or return existing if same file already open) */
		addHtmlPreview(repoPath: string, filePath: string, fsRoot?: string): string {
			const effectiveRoot = fsRoot || repoPath;
			const existing = Object.values(base.state.tabs).find(
				(tab) =>
					tab.type === "html-preview" && (tab as HtmlPreviewTab).fsRoot === effectiveRoot && tab.filePath === filePath,
			) as HtmlPreviewTab | undefined;
			if (existing) {
				const key = currentBranchKey();
				if (existing.repoPath !== repoPath || existing.branchKey !== key) {
					base._setState("tabs", existing.id, "repoPath" as keyof MdTabData, repoPath as MdTabData[keyof MdTabData]);
					base._setState("tabs", existing.id, "branchKey" as keyof MdTabData, key as MdTabData[keyof MdTabData]);
				}
				base.setActive(existing.id);
				return existing.id;
			}

			const id = base._nextId("md");
			const fileName = pathBasename(filePath) || filePath;
			const tab: HtmlPreviewTab = {
				type: "html-preview",
				id,
				title: fileName,
				repoPath,
				filePath,
				fileName,
				branchKey: currentBranchKey(),
				fsRoot: effectiveRoot,
			};
			return base._addTab(tab);
		},

		/** Set a tab's font size (clamped to [MIN, MAX]). */
		setFontSize(id: string, fontSize: number): void {
			if (!base.state.tabs[id]) return;
			const clamped = Math.max(MD_MIN_FONT_SIZE, Math.min(MD_MAX_FONT_SIZE, fontSize));
			base._setState("tabs", id, "fontSize" as keyof MdTabData, clamped as MdTabData[keyof MdTabData]);
		},

		/** Get a tab's effective font size (falls back to default). */
		getFontSize(id: string): number {
			return base.state.tabs[id]?.fontSize ?? MD_DEFAULT_FONT_SIZE;
		},

		/** Zoom in the active markdown tab by one step. */
		zoomIn(): void {
			const id = base.state.activeId;
			if (!id) return;
			this.setFontSize(id, (base.state.tabs[id]?.fontSize ?? MD_DEFAULT_FONT_SIZE) + MD_FONT_STEP);
		},

		/** Zoom out the active markdown tab by one step. */
		zoomOut(): void {
			const id = base.state.activeId;
			if (!id) return;
			this.setFontSize(id, (base.state.tabs[id]?.fontSize ?? MD_DEFAULT_FONT_SIZE) - MD_FONT_STEP);
		},

		/** Reset the active markdown tab to the default font size. */
		zoomReset(): void {
			const id = base.state.activeId;
			if (!id) return;
			this.setFontSize(id, MD_DEFAULT_FONT_SIZE);
		},

		/** Register an imperative handle for a tab (e.g. MarkdownTabHandle with openSearch) */
		setHandle(tabId: string, handle: unknown): void {
			handles.set(tabId, handle);
		},

		/** Remove the imperative handle when a tab component unmounts */
		clearHandle(tabId: string): void {
			handles.delete(tabId);
		},

		/** Retrieve the imperative handle for a tab */
		getHandle<T = unknown>(tabId: string): T | undefined {
			return handles.get(tabId) as T | undefined;
		},

		/**
		 * Evict non-pinned plugin-panel tabs whose repoPath is set and doesn't match
		 * the given repo. Without this, every visited repo leaves a stale UI tab in
		 * state.tabs forever — invisible (getVisibleIds hides them) but still holding
		 * HTML in memory. Called on repo switch. Pinned tabs and tabs with no
		 * repoPath (globally scoped) are preserved. Non-plugin-panel tabs (file,
		 * virtual, pr-diff, html-preview, etc.) are untouched — they have their own
		 * lifecycle and users rely on them persisting across repo switches.
		 */
		evictNonPinnedPluginPanelsForOtherRepos(currentRepoPath: string | null): void {
			base._clearWhere(
				(tab) => tab.type === "plugin-panel" && !tab.pinned && !!tab.repoPath && tab.repoPath !== currentRepoPath,
			);
		},

		/** Clear all file-based markdown tabs for a repository (virtual tabs are unaffected) */
		clearForRepo(repoPath: string): void {
			base._clearWhere((tab) => tab.type === "file" && (tab as FileTab).repoPath === repoPath);
		},

		/** Get file-based tabs for a specific repository */
		getForRepo(repoPath: string): FileTab[] {
			return Object.values(base.state.tabs).filter(
				(tab): tab is FileTab => tab.type === "file" && tab.repoPath === repoPath,
			);
		},
	};
}

export const mdTabsStore = createMdTabsStore();
