/**
 * Client-side tab cache for instant tab and space switching.
 *
 * Instead of full page navigations, previously loaded tabs are kept in the DOM
 * and shown/hidden via CSS. New tabs are fetched via fetch() + DOMParser on
 * first visit, then stay in the DOM forever.
 *
 * Pane keys are `${spaceSlug}:${moduleId}` so panes from different spaces
 * coexist in the DOM. On space switch, current space's panes are hidden and
 * the target space's panes are shown (or fetched on first visit).
 *
 * Hiding preserves ALL component state: Shadow DOM, event listeners,
 * WebSocket connections, timers.
 */

export class TabCache {
	private currentModuleId: string;
	private spaceSlug: string;
	private panes = new Map<string, HTMLElement>();
	private fetchController: AbortController | null = null;

	constructor(spaceSlug: string, moduleId: string) {
		this.spaceSlug = spaceSlug;
		this.currentModuleId = moduleId;
	}

	/** Current space slug */
	get currentSpace(): string {
		return this.spaceSlug;
	}

	/** Current module ID */
	get currentModule(): string {
		return this.currentModuleId;
	}

	/** Build the composite pane key */
	private paneKey(space: string, moduleId: string): string {
		return `${space}:${moduleId}`;
	}

	/** Wrap the initial #app content in a pane div and set up popstate */
	init(): boolean {
		const app = document.getElementById("app");
		if (!app) return false;

		const key = this.paneKey(this.spaceSlug, this.currentModuleId);

		// Wrap existing content in a pane
		const pane = document.createElement("div");
		pane.className = "rspace-tab-pane rspace-tab-pane--active";
		pane.dataset.moduleId = this.currentModuleId;
		pane.dataset.spaceSlug = this.spaceSlug;
		pane.dataset.pageTitle = document.title;

		while (app.firstChild) {
			pane.appendChild(app.firstChild);
		}
		app.appendChild(pane);
		this.panes.set(key, pane);

		// Push initial state into history
		history.replaceState(
			{ moduleId: this.currentModuleId, spaceSlug: this.spaceSlug },
			"",
			window.location.href,
		);

		// Handle browser back/forward
		window.addEventListener("popstate", (e) => {
			const state = e.state;
			if (state?.dashboard) {
				// Dashboard state — hide all panes and show dashboard
				this.hideAllPanes();
				const dashboard = document.querySelector("rstack-user-dashboard");
				if (dashboard) (dashboard as HTMLElement).style.display = "";
				return;
			}
			if (!state?.moduleId) {
				window.location.reload();
				return;
			}

			// Intra-module view navigation (back within an rApp)
			// If same module+space and the state has a _view field, dispatch to
			// ViewHistory instead of switching panes.
			const stateSpace = state.spaceSlug || this.spaceSlug;
			if (state.moduleId === this.currentModuleId &&
				stateSpace === this.spaceSlug &&
				('_view' in state || (e as any)._viewNav)) {
				window.dispatchEvent(new CustomEvent('rspace-view-popstate', {
					detail: { moduleId: state.moduleId, view: state._view, context: state._viewCtx },
				}));
				return;
			}

			// If returning from dashboard, hide it and clear home-active
			const dashboard = document.querySelector("rstack-user-dashboard");
			if (dashboard) (dashboard as HTMLElement).style.display = "none";
			document.querySelector("rstack-tab-bar")?.setAttribute("home-active", "false");
			const key = this.paneKey(stateSpace, state.moduleId);
			if (this.panes.has(key)) {
				if (stateSpace !== this.spaceSlug) {
					this.hideAllPanes();
					this.spaceSlug = stateSpace;
				}
				this.showPane(stateSpace, state.moduleId);
			} else {
				window.location.reload();
			}
		});

		return true;
	}

	/** Switch to a module tab within the current space. Returns true if handled client-side. */
	async switchTo(moduleId: string): Promise<boolean> {
		// Abort any in-flight fetch from a previous switchTo call
		if (this.fetchController) {
			this.fetchController.abort();
			this.fetchController = null;
		}

		const key = this.paneKey(this.spaceSlug, moduleId);
		if (moduleId === this.currentModuleId && this.panes.has(key)) {
			console.log("[TabCache] switchTo", moduleId, "→ already current + cached");
			return true;
		}

		if (this.panes.has(key)) {
			console.log("[TabCache] switchTo", moduleId, "→ cached, showing pane");
			this.showPane(this.spaceSlug, moduleId);
			this.updateUrl(this.spaceSlug, moduleId);
			return true;
		}

		console.log("[TabCache] switchTo", moduleId, "→ NOT cached, fetching…");
		return this.fetchAndInject(this.spaceSlug, moduleId);
	}

	/**
	 * Switch to a different space, loading a module within it.
	 * Hides all panes from the current space, shows/fetches the target space's pane.
	 * Returns true if handled client-side.
	 */
	async switchSpace(newSpace: string, moduleId: string): Promise<boolean> {
		if (newSpace === this.spaceSlug && moduleId === this.currentModuleId) return true;

		const key = this.paneKey(newSpace, moduleId);

		// Hide all current panes
		this.hideAllPanes();

		// Update active space
		const oldSpace = this.spaceSlug;
		this.spaceSlug = newSpace;

		if (this.panes.has(key)) {
			this.showPane(newSpace, moduleId);
			this.updateUrl(newSpace, moduleId);
			this.updateSpaceChrome(newSpace);
			return true;
		}

		const ok = await this.fetchAndInject(newSpace, moduleId);
		if (ok) {
			this.updateSpaceChrome(newSpace);
			return true;
		}

		// Rollback on failure
		this.spaceSlug = oldSpace;
		return false;
	}

	/** Check if a pane exists in the DOM cache */
	has(moduleId: string): boolean {
		return this.panes.has(this.paneKey(this.spaceSlug, moduleId));
	}

	/** Remove a cached pane from the DOM */
	removePane(moduleId: string): void {
		const key = this.paneKey(this.spaceSlug, moduleId);
		const pane = this.panes.get(key);
		if (pane) {
			pane.remove();
			this.panes.delete(key);
		}
	}

	/** Fetch a module page, extract content, and inject into a new pane */
	private async fetchAndInject(space: string, moduleId: string): Promise<boolean> {
		const navUrl = (window as any).__rspaceNavUrl;
		if (!navUrl) { console.warn("[TabCache] fetchAndInject: no __rspaceNavUrl"); return false; }
		const url: string = navUrl(space, moduleId);

		// For cross-space fetches, use the path-based API to stay same-origin
		let fetchUrl = url;
		try {
			const resolved = new URL(url, window.location.href);
			if (resolved.origin !== window.location.origin) {
				fetchUrl = `/${space}/${moduleId}`;
			}
		} catch {
			console.warn("[TabCache] fetchAndInject: URL resolve failed for", url);
			return false;
		}

		const app = document.getElementById("app");
		if (!app) { console.warn("[TabCache] fetchAndInject: no #app"); return false; }

		// Append ?fragment=1 for lightweight JSON response (no full shell re-render)
		const fragmentUrl = fetchUrl + (fetchUrl.includes("?") ? "&" : "?") + "fragment=1";
		console.log("[TabCache] fetchAndInject:", fragmentUrl, "for", moduleId);

		// Show loading spinner
		const loadingPane = document.createElement("div");
		loadingPane.className =
			"rspace-tab-pane rspace-tab-pane--active rspace-tab-loading";
		loadingPane.innerHTML = '<div class="rspace-tab-spinner"></div>';
		this.hideAllPanes();
		app.appendChild(loadingPane);

		// Create an abort controller that also times out after 15s
		const controller = new AbortController();
		this.fetchController = controller;
		const timeoutId = setTimeout(() => controller.abort(), 15_000);

		try {
			const resp = await fetch(fragmentUrl, {
				signal: controller.signal,
			});
			if (!resp.ok) {
				clearTimeout(timeoutId);
				console.warn("[TabCache] fetchAndInject: HTTP", resp.status, "for", fragmentUrl);
				loadingPane.remove();
				return false;
			}

			clearTimeout(timeoutId);
			const ct = resp.headers.get("content-type") || "";
			let content: { body: string; title: string; scripts: string[]; styles: string[]; inlineStyles: string[] } | null;

			if (ct.includes("application/json")) {
				// Fast path: server returned pre-extracted JSON fragment
				content = await resp.json();
				console.log("[TabCache] fetchAndInject: got JSON fragment for", moduleId);
			} else {
				// Fallback: server returned full HTML (fragment middleware didn't match)
				const html = await resp.text();
				console.log("[TabCache] fetchAndInject: got", html.length, "bytes HTML (fallback) for", moduleId);
				content = this.extractContent(html);
			}

			if (!content) {
				console.warn("[TabCache] fetchAndInject: no content extracted for", moduleId);
				loadingPane.remove();
				return false;
			}

			// Remove loading spinner
			loadingPane.remove();

			const key = this.paneKey(space, moduleId);

			// Create the real pane
			const pane = document.createElement("div");
			pane.className = "rspace-tab-pane rspace-tab-pane--active";
			pane.dataset.moduleId = moduleId;
			pane.dataset.spaceSlug = space;
			pane.dataset.pageTitle = content.title;
			// Wrap body in .rapp-content so the flex layout + scrolling works
			const contentWrap = document.createElement("div");
			contentWrap.className = "rapp-content";
			contentWrap.innerHTML = content.body;
			pane.appendChild(contentWrap);
			app.appendChild(pane);
			this.panes.set(key, pane);

			// Load module-specific assets
			this.loadAssets(content.scripts, content.styles, content.inlineStyles, `${space}-${moduleId}`);

			// Update shell state
			this.currentModuleId = moduleId;
			document.title = content.title;
			this.updateShellState(moduleId);
			this.updateUrl(space, moduleId);
			this.updateCanvasLayout(moduleId);

			console.log("[TabCache] fetchAndInject: SUCCESS for", moduleId);
			return true;
		} catch (err) {
			clearTimeout(timeoutId);
			loadingPane.remove();
			// If aborted because the user switched to a different tab, return
			// "true" so the shell doesn't trigger a fallback full-page navigation
			// for a tab the user no longer wants.
			if (controller.signal.aborted && this.fetchController !== controller) {
				console.log("[TabCache] fetchAndInject: aborted (superseded) for", moduleId);
				return true;
			}
			console.error("[TabCache] fetchAndInject: CATCH for", moduleId, err);
			return false;
		}
	}

	/** Parse full HTML response and extract module content */
	private extractContent(
		html: string,
	): {
		body: string;
		title: string;
		scripts: string[];
		styles: string[];
		inlineStyles: string[];
	} | null {
		const parser = new DOMParser();
		const doc = parser.parseFromString(html, "text/html");

		// Extract #app content or .rspace-iframe-wrap (for external app modules)
		const appEl = doc.getElementById("app");
		const iframeWrap = doc.querySelector(".rspace-iframe-wrap");

		let body: string;
		if (appEl) {
			// Remove duplicate shell chrome that shouldn't be in tab panes
			appEl.querySelectorAll(
				".rstack-tab-row, rstack-tab-bar, rstack-history-panel",
			).forEach((el) => el.remove());
			body = appEl.innerHTML;
		} else if (iframeWrap) {
			body = iframeWrap.outerHTML;
		} else {
			return null;
		}

		const title = doc.title || "";

		// Extract module-specific scripts (exclude shell.js and inline shell bootstrap)
		const scripts: string[] = [];
		doc.querySelectorAll('script[type="module"]').forEach((s) => {
			const src = s.getAttribute("src") || "";
			if (src.includes("shell.js")) return;
			const text = s.textContent || "";
			if (text.includes("import '/shell.js")) return;
			if (text.includes('import "/shell.js')) return;
			if (src) scripts.push(src);
		});

		// Extract module-specific styles (exclude shell.css)
		const styles: string[] = [];
		doc.querySelectorAll('link[rel="stylesheet"]').forEach((l) => {
			const href = l.getAttribute("href") || "";
			if (href.includes("shell.css")) return;
			styles.push(href);
		});

		// Extract inline <style> blocks (exclude shell-embedded styles)
		const inlineStyles: string[] = [];
		doc.querySelectorAll("head style").forEach((s) => {
			const css = s.textContent || "";
			// Skip the shell's own embedded styles (tab-pane, spinner, etc.)
			if (css.includes(".rspace-tab-pane")) return;
			if (css.trim()) inlineStyles.push(css);
		});

		return { body, title, scripts, styles, inlineStyles };
	}

	/** Load script and style assets (idempotent — browsers deduplicate module scripts) */
	private loadAssets(scripts: string[], styles: string[], inlineStyles: string[] = [], moduleId?: string): void {
		for (const src of scripts) {
			const el = document.createElement("script");
			el.type = "module";
			el.src = src;
			// If a script fails (502, network error), evict the pane from cache
			// so the next switchTo re-fetches everything instead of showing a blank pane.
			el.onerror = () => {
				console.warn("[TabCache] script load failed:", src, "— evicting pane", moduleId);
				el.remove();
				if (moduleId) {
					// moduleId here is "space-module" format — find and remove the pane
					for (const [key, pane] of this.panes) {
						if (pane.dataset.moduleId && moduleId.endsWith(pane.dataset.moduleId)) {
							pane.remove();
							this.panes.delete(key);
							break;
						}
					}
				}
			};
			document.body.appendChild(el);
		}

		for (const href of styles) {
			if (document.querySelector(`link[href="${CSS.escape(href)}"]`))
				continue;
			const el = document.createElement("link");
			el.rel = "stylesheet";
			el.href = href;
			document.head.appendChild(el);
		}

		// Inject inline <style> blocks (tagged for deduplication)
		for (let i = 0; i < inlineStyles.length; i++) {
			const tag = `tab-style-${moduleId || "unknown"}-${i}`;
			if (document.querySelector(`style[data-tab-style="${tag}"]`)) continue;
			const el = document.createElement("style");
			el.dataset.tabStyle = tag;
			el.textContent = inlineStyles[i];
			document.head.appendChild(el);
		}
	}

	/** Show a specific pane and update shell state */
	private showPane(space: string, moduleId: string): void {
		this.hideAllPanes();
		const key = this.paneKey(space, moduleId);
		const pane = this.panes.get(key);
		if (pane) {
			pane.classList.add("rspace-tab-pane--active");
			const storedTitle = pane.dataset.pageTitle;
			if (storedTitle) document.title = storedTitle;
		}
		this.currentModuleId = moduleId;
		this.updateShellState(moduleId);
		this.updateCanvasLayout(moduleId);
	}

	/** Hide all panes (public so the shell can call it for dashboard) */
	hideAllPanes(): void {
		const app = document.getElementById("app");
		if (!app) return;
		app.querySelectorAll(".rspace-tab-pane").forEach((p) => {
			p.classList.remove("rspace-tab-pane--active");
		});
	}

	/** Update app-switcher and tab-bar to reflect current module */
	private updateShellState(moduleId: string): void {
		const appSwitcher = document.querySelector("rstack-app-switcher");
		if (appSwitcher) appSwitcher.setAttribute("current", moduleId);

		const tabBar = (window as any).__rspaceTabBar;
		if (tabBar) tabBar.setAttribute("active", "layer-" + moduleId);
	}

	/** Toggle canvas-layout class on #app */
	private updateCanvasLayout(moduleId: string): void {
		const app = document.getElementById("app");
		if (!app) return;
		if (moduleId === "rspace") {
			app.classList.add("canvas-layout");
		} else {
			app.classList.remove("canvas-layout");
		}
	}

	/** Update space-specific shell chrome (space switcher, body attrs) */
	private updateSpaceChrome(newSpace: string): void {
		// Update space-switcher attribute
		const spaceSwitcher = document.querySelector("rstack-space-switcher");
		if (spaceSwitcher) {
			spaceSwitcher.setAttribute("current", newSpace);
			spaceSwitcher.setAttribute("name", newSpace);
		}

		// Update body data attribute
		document.body?.setAttribute("data-space-slug", newSpace);
	}

	/** Push new URL to browser history */
	private updateUrl(space: string, moduleId: string): void {
		const navUrl = (window as any).__rspaceNavUrl;
		if (!navUrl) return;
		const url: string = navUrl(space, moduleId);
		history.pushState(
			{ moduleId, spaceSlug: space },
			"",
			url,
		);
	}
}
