/**
 * EcosystemBridge — Loads external r-ecosystem app manifests and dynamically
 * imports their Web Component modules for canvas embedding.
 *
 * Two embedding modes:
 *   1. Trusted: dynamic import() → registers Web Component → shares CRDT + ports + events
 *   2. Sandboxed: creates iframe with sandbox attr → postMessage bridge → origin-validated
 *
 * Usage:
 *   const bridge = EcosystemBridge.getInstance();
 *   const manifest = await bridge.loadManifest("rwallet");
 *   await bridge.loadModule(manifest);            // registers shapes in shapeRegistry
 *   // or
 *   const iframe = bridge.createSandboxedEmbed(manifest, shapeDesc, container);
 */

import type {
	EcosystemManifest,
	ResolvedManifest,
	EcosystemShapeDescriptor,
} from "../shared/ecosystem-manifest";
import { ECOSYSTEM_PROTOCOL_VERSION } from "../shared/ecosystem-manifest";
import { shapeRegistry } from "./shape-registry";

// ── Types ──

interface CachedManifest {
	manifest: ResolvedManifest;
	fetchedAt: number;
}

interface SandboxedEmbed {
	iframe: HTMLIFrameElement;
	appId: string;
	origin: string;
	destroy: () => void;
}

// ── Singleton ──

let instance: EcosystemBridge | null = null;

export class EcosystemBridge {
	/** Cached manifests keyed by appId */
	#manifests = new Map<string, CachedManifest>();

	/** Loaded module URLs (to avoid double-import) */
	#loadedModules = new Set<string>();

	/** Active sandboxed embeds */
	#sandboxedEmbeds = new Map<string, SandboxedEmbed>();

	/** postMessage handler bound reference for cleanup */
	#messageHandler: ((e: MessageEvent) => void) | null = null;

	/** Cache TTL for manifests (1 hour) */
	static MANIFEST_TTL = 3600_000;

	static getInstance(): EcosystemBridge {
		if (!instance) {
			instance = new EcosystemBridge();
		}
		return instance;
	}

	constructor() {
		// Listen for postMessage from sandboxed iframes
		this.#messageHandler = this.#handleSandboxMessage.bind(this);
		if (typeof window !== "undefined") {
			window.addEventListener("message", this.#messageHandler);
		}
	}

	// ── Manifest loading ──

	/**
	 * Load an ecosystem app's manifest via the server proxy (avoids CORS).
	 * Results are cached for MANIFEST_TTL.
	 */
	async loadManifest(appId: string): Promise<ResolvedManifest> {
		// Check cache
		const cached = this.#manifests.get(appId);
		if (cached && Date.now() - cached.fetchedAt < EcosystemBridge.MANIFEST_TTL) {
			return cached.manifest;
		}

		const res = await fetch(`/api/ecosystem/${encodeURIComponent(appId)}/manifest`);
		if (!res.ok) {
			throw new Error(`Failed to load manifest for ${appId}: ${res.status}`);
		}

		const data: ResolvedManifest = await res.json();

		// Validate protocol version
		if (data.minProtocolVersion && data.minProtocolVersion > ECOSYSTEM_PROTOCOL_VERSION) {
			throw new Error(
				`App ${appId} requires protocol v${data.minProtocolVersion}, ` +
				`but this rSpace supports v${ECOSYSTEM_PROTOCOL_VERSION}`
			);
		}

		this.#manifests.set(appId, { manifest: data, fetchedAt: Date.now() });

		return data;
	}

	/** Get a cached manifest without fetching. */
	getCachedManifest(appId: string): ResolvedManifest | null {
		return this.#manifests.get(appId)?.manifest ?? null;
	}

	/** Get all cached manifests. */
	getAllCachedManifests(): ResolvedManifest[] {
		return Array.from(this.#manifests.values()).map((c) => c.manifest);
	}

	// ── Module loading (Trusted mode) ──

	/**
	 * Dynamically import an ecosystem module and register its shapes.
	 * Uses the server module proxy to avoid CORS.
	 *
	 * The imported module is expected to:
	 *   - Define and register custom elements (Web Components)
	 *   - Export shape classes that extend FolkShape or HTMLElement
	 */
	async loadModule(manifest: ResolvedManifest): Promise<void> {
		const moduleUrl = `/api/ecosystem/${encodeURIComponent(manifest.appId)}/module`;

		if (this.#loadedModules.has(manifest.appId)) {
			return; // Already loaded
		}

		try {
			// Dynamic import via blob URL to execute the proxied JS as a module
			const res = await fetch(moduleUrl);
			if (!res.ok) throw new Error(`Module fetch failed: ${res.status}`);

			const js = await res.text();
			const blob = new Blob([js], { type: "application/javascript" });
			const blobUrl = URL.createObjectURL(blob);

			try {
				await import(/* @vite-ignore */ blobUrl);
			} finally {
				URL.revokeObjectURL(blobUrl);
			}

			this.#loadedModules.add(manifest.appId);

			// Register shapes in the shape registry (if not already registered by the module itself)
			this.registerShapes(manifest);

			// Notify service worker to cache the module
			this.#notifySwCache(manifest);
		} catch (err) {
			console.error(`[EcosystemBridge] Failed to load module for ${manifest.appId}:`, err);
			throw err;
		}
	}

	/**
	 * Register manifest shapes in the shape registry.
	 * Called automatically by loadModule(), but can also be called manually
	 * if the module was loaded via another mechanism (e.g. <script> tag).
	 */
	registerShapes(manifest: ResolvedManifest): void {
		for (const shape of manifest.shapes) {
			if (shapeRegistry.has(shape.tagName)) continue; // Already registered

			// The module should have called customElements.define() already
			const elementClass = customElements.get(shape.tagName);
			if (elementClass) {
				shapeRegistry.register(shape.tagName, elementClass as any);
			}
		}
	}

	/** Check if a module has been loaded. */
	isModuleLoaded(appId: string): boolean {
		return this.#loadedModules.has(appId);
	}

	// ── Sandboxed mode (iframe) ──

	/**
	 * Create a sandboxed iframe embed for untrusted ecosystem apps.
	 * The iframe gets a restricted sandbox and communicates via validated postMessage.
	 */
	createSandboxedEmbed(
		manifest: ResolvedManifest,
		shapeDesc: EcosystemShapeDescriptor,
		container: HTMLElement,
		config?: { shapeId?: string; space?: string }
	): SandboxedEmbed {
		const iframe = document.createElement("iframe");
		iframe.className = "ecosystem-sandbox";

		// Sandboxed: allow scripts + same-origin forms, but no top-nav, popups, etc.
		iframe.sandbox.add("allow-scripts");
		iframe.sandbox.add("allow-forms");

		// The iframe loads the app's dedicated embed page (or homepage with embed param)
		const embedUrl = new URL(manifest.homepage);
		embedUrl.searchParams.set("embed", "true");
		embedUrl.searchParams.set("shape", shapeDesc.tagName);
		if (config?.shapeId) embedUrl.searchParams.set("shapeId", config.shapeId);
		if (config?.space) embedUrl.searchParams.set("space", config.space);
		iframe.src = embedUrl.toString();

		iframe.style.cssText = "width: 100%; height: 100%; border: none;";
		iframe.loading = "lazy";

		// Send context once loaded
		iframe.addEventListener("load", () => {
			this.#sendSandboxContext(iframe, manifest, shapeDesc, config);
		});

		container.appendChild(iframe);

		const embed: SandboxedEmbed = {
			iframe,
			appId: manifest.appId,
			origin: manifest.origin,
			destroy: () => {
				iframe.remove();
				this.#sandboxedEmbeds.delete(embedKey);
			},
		};

		const embedKey = `${manifest.appId}:${config?.shapeId || "default"}`;
		this.#sandboxedEmbeds.set(embedKey, embed);

		return embed;
	}

	/** Send context to a sandboxed iframe with origin validation. */
	#sendSandboxContext(
		iframe: HTMLIFrameElement,
		manifest: ResolvedManifest,
		shapeDesc: EcosystemShapeDescriptor,
		config?: { shapeId?: string; space?: string }
	) {
		if (!iframe.contentWindow) return;
		try {
			iframe.contentWindow.postMessage(
				{
					source: "rspace-parent",
					type: "context",
					protocol: ECOSYSTEM_PROTOCOL_VERSION,
					appId: manifest.appId,
					shapeId: config?.shapeId || null,
					space: config?.space || null,
					shapeName: shapeDesc.tagName,
					portDescriptors: shapeDesc.portDescriptors,
					eventDescriptors: shapeDesc.eventDescriptors,
				},
				manifest.origin // Origin-validated — NOT "*"
			);
		} catch {
			// Cross-origin or iframe not ready
		}
	}

	/** Handle postMessage from sandboxed iframes with origin validation. */
	#handleSandboxMessage(e: MessageEvent) {
		const msg = e.data;
		if (!msg || typeof msg !== "object") return;
		if (msg.source !== "rspace-ecosystem-embed") return;

		// Validate origin against known ecosystem apps
		const embed = Array.from(this.#sandboxedEmbeds.values()).find(
			(em) => em.origin === e.origin
		);
		if (!embed) {
			console.warn(`[EcosystemBridge] Rejected message from unknown origin: ${e.origin}`);
			return;
		}

		switch (msg.type) {
			case "port-update":
				// Sandboxed app wants to update a port value
				// Dispatch as a CustomEvent on the parent document
				window.dispatchEvent(
					new CustomEvent("ecosystem-port-update", {
						detail: {
							appId: embed.appId,
							shapeId: msg.shapeId,
							portName: msg.portName,
							value: msg.value,
						},
					})
				);
				break;

			case "event-emit":
				// Sandboxed app wants to emit an event bus channel
				window.dispatchEvent(
					new CustomEvent("ecosystem-event-emit", {
						detail: {
							appId: embed.appId,
							shapeId: msg.shapeId,
							channel: msg.channel,
							payload: msg.payload,
						},
					})
				);
				break;

			case "ready":
				// Sandboxed embed signaling it's initialized
				console.log(`[EcosystemBridge] ${embed.appId} embed ready`);
				break;
		}
	}

	// ── Service Worker cache notification ──

	#notifySwCache(manifest: ResolvedManifest) {
		if (!("serviceWorker" in navigator) || !navigator.serviceWorker.controller) return;
		navigator.serviceWorker.controller.postMessage({
			type: "cache-ecosystem-module",
			appId: manifest.appId,
			moduleUrl: manifest.resolvedModuleUrl,
			origin: manifest.origin,
		});
	}

	// ── Cleanup ──

	destroy() {
		if (this.#messageHandler && typeof window !== "undefined") {
			window.removeEventListener("message", this.#messageHandler);
		}
		for (const embed of this.#sandboxedEmbeds.values()) {
			embed.iframe.remove();
		}
		this.#sandboxedEmbeds.clear();
		this.#manifests.clear();
		this.#loadedModules.clear();
		instance = null;
	}
}
