import { FolkShape } from "./folk-shape";
import { css, html } from "./tags";
import { rspaceNavUrl } from "../shared/url-helpers";
import { MODULE_META } from "./module-display";
import type { PortDescriptor } from "./data-types";

/**
 * <folk-rapp> — Embeds a live rApp module as a shape on the canvas.
 *
 * Unlike folk-embed (generic URL iframe), folk-rapp understands the module
 * system: it stores moduleId + spaceSlug, derives the iframe URL, shows
 * the module's icon/badge in the header, and can switch modules in-place.
 *
 * PostMessage protocol:
 *   Parent → iframe: { source: "rspace-parent", type: "context", shapeId, space, moduleId }
 *   iframe → parent: { source: "rspace-canvas", type: "shape-updated", ... } (from CommunitySync)
 *   iframe → parent: { source: "rspace-rapp", type: "navigate", moduleId }
 */

const styles = css`
	:host {
		display: flex;
		flex-direction: column;
		background: var(--rs-bg-surface);
		border-radius: 10px;
		box-shadow: var(--rs-shadow-md);
		min-width: 200px;
		min-height: 120px;
		overflow: hidden;
	}

	.rapp-wrapper {
		display: flex;
		flex-direction: column;
		height: 100%;
		width: 100%;
	}

	.rapp-header {
		display: flex;
		align-items: center;
		justify-content: space-between;
		padding: 6px 10px;
		background: var(--rapp-color, var(--rs-bg-surface-raised));
		color: var(--rs-text-inverse);
		font-size: 12px;
		font-weight: 700;
		cursor: move;
		user-select: none;
		border-radius: 10px 10px 0 0;
		flex-shrink: 0;
	}

	.rapp-header-left {
		display: flex;
		align-items: center;
		gap: 7px;
		position: relative;
	}

	.rapp-badge {
		display: flex;
		align-items: center;
		justify-content: center;
		width: 22px;
		height: 22px;
		border-radius: 5px;
		background: rgba(0, 0, 0, 0.15);
		font-size: 0.55rem;
		font-weight: 900;
		line-height: 1;
		flex-shrink: 0;
	}

	.rapp-name {
		font-size: 12px;
		font-weight: 700;
	}

	.rapp-icon {
		font-size: 13px;
	}

	.rapp-actions {
		display: flex;
		gap: 2px;
	}

	.rapp-actions button {
		background: transparent;
		border: none;
		color: var(--rs-text-inverse);
		cursor: pointer;
		padding: 2px 5px;
		border-radius: 4px;
		font-size: 12px;
		opacity: 0.7;
		transition: opacity 0.15s, background 0.15s;
	}

	.rapp-actions button:hover {
		opacity: 1;
		background: rgba(0, 0, 0, 0.1);
	}

	.rapp-content {
		flex: 1;
		position: relative;
		overflow: hidden;
	}

	/* Transparent overlay intercepts wheel/pointer events so canvas zoom + selection work */
	.rapp-overlay {
		position: absolute;
		inset: 0;
		z-index: 10;
		background: transparent;
		cursor: move;
	}

	/* In editing mode, hide overlay so iframe becomes interactive */
	:host(:state(editing)) .rapp-overlay {
		display: none;
	}

	.rapp-iframe {
		width: 100%;
		height: 100%;
		border: none;
		border-radius: 0 0 10px 10px;
	}

	.rapp-loading {
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: center;
		height: 100%;
		gap: 12px;
		color: var(--rs-text-muted);
		font-size: 13px;
	}

	.rapp-loading .spinner {
		width: 24px;
		height: 24px;
		border: 2px solid var(--rs-spinner-track);
		border-top-color: var(--rs-text-muted);
		border-radius: 50%;
		animation: spin 0.8s linear infinite;
	}

	@keyframes spin {
		to { transform: rotate(360deg); }
	}

	.rapp-error {
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: center;
		height: 100%;
		gap: 8px;
		color: var(--rs-error);
		font-size: 13px;
		padding: 16px;
		text-align: center;
	}

	.rapp-disabled {
		display: flex;
		flex-direction: column;
		align-items: center;
		justify-content: center;
		height: 100%;
		gap: 8px;
		color: var(--rs-text-muted);
		font-size: 13px;
		padding: 16px;
		text-align: center;
	}

	.rapp-disabled-icon {
		font-size: 28px;
		opacity: 0.4;
	}

	:host([data-module-disabled]) .rapp-header {
		filter: grayscale(0.6);
		opacity: 0.6;
	}

	/* Module picker (shown when no moduleId set) */
	.rapp-picker {
		display: flex;
		flex-direction: column;
		gap: 4px;
		padding: 12px;
		height: 100%;
		overflow-y: auto;
	}

	.rapp-picker-title {
		font-size: 12px;
		font-weight: 600;
		color: var(--rs-text-secondary);
		margin-bottom: 4px;
	}

	.rapp-picker-item {
		display: flex;
		align-items: center;
		gap: 8px;
		padding: 6px 10px;
		border-radius: 6px;
		cursor: pointer;
		transition: background 0.12s;
		color: var(--rs-text-primary);
		font-size: 13px;
		border: none;
		background: transparent;
		text-align: left;
		width: 100%;
	}

	.rapp-picker-item:hover {
		background: var(--rs-bg-hover);
	}

	.rapp-picker-badge {
		display: flex;
		align-items: center;
		justify-content: center;
		width: 24px;
		height: 24px;
		border-radius: 5px;
		font-size: 0.5rem;
		font-weight: 900;
		color: var(--rs-text-inverse);
		flex-shrink: 0;
	}

	/* Module switcher dropdown */
	.rapp-switcher {
		position: absolute;
		top: 100%;
		left: 0;
		margin-top: 4px;
		min-width: 180px;
		max-height: 300px;
		overflow-y: auto;
		background: var(--rs-bg-surface);
		border: 1px solid var(--rs-border);
		border-radius: 8px;
		box-shadow: var(--rs-shadow-lg);
		padding: 4px;
		z-index: 100;
		display: none;
	}

	.rapp-switcher.open {
		display: block;
	}

	.rapp-switcher-item {
		display: flex;
		align-items: center;
		gap: 6px;
		padding: 5px 8px;
		border-radius: 5px;
		cursor: pointer;
		color: var(--rs-text-primary);
		font-size: 12px;
		border: none;
		background: transparent;
		width: 100%;
		text-align: left;
		transition: background 0.12s;
	}

	.rapp-switcher-item:hover {
		background: var(--rs-bg-hover);
	}

	.rapp-switcher-item.active {
		background: var(--rs-bg-active);
	}

	.rapp-switcher-badge {
		display: flex;
		align-items: center;
		justify-content: center;
		width: 18px;
		height: 18px;
		border-radius: 4px;
		font-size: 0.45rem;
		font-weight: 900;
		color: var(--rs-text-inverse);
		flex-shrink: 0;
	}

	/* Status indicator for postMessage connection */
	.rapp-status {
		width: 6px;
		height: 6px;
		border-radius: 50%;
		background: var(--rs-text-muted);
		flex-shrink: 0;
		transition: background 0.3s;
	}

	.rapp-status.connected {
		background: var(--rs-success);
	}

	/* Widget mode styles */
	.rapp-widget {
		display: flex;
		flex-direction: column;
		height: 100%;
		padding: 10px 12px;
		color: var(--rs-text-primary);
		font-size: 13px;
		cursor: pointer;
		overflow: hidden;
	}

	.rapp-widget:hover {
		background: var(--rs-bg-hover);
	}

	.rapp-widget-stat {
		font-size: 20px;
		font-weight: 700;
		color: var(--rs-text-primary);
		margin-bottom: 6px;
	}

	.rapp-widget-divider {
		border: none;
		border-top: 1px solid var(--rs-border-subtle);
		margin: 6px 0;
	}

	.rapp-widget-list {
		display: flex;
		flex-direction: column;
		gap: 3px;
		flex: 1;
		overflow: hidden;
	}

	.rapp-widget-row {
		display: flex;
		align-items: center;
		gap: 6px;
		font-size: 12px;
		color: var(--rs-text-secondary);
		white-space: nowrap;
		overflow: hidden;
		text-overflow: ellipsis;
	}

	.rapp-widget-row .bullet {
		color: var(--rs-text-muted);
		flex-shrink: 0;
	}

	.rapp-widget-row .label {
		overflow: hidden;
		text-overflow: ellipsis;
	}

	.rapp-widget-row .value {
		margin-left: auto;
		color: var(--rs-text-secondary);
		flex-shrink: 0;
	}

	.rapp-widget-empty {
		color: var(--rs-text-muted);
		font-size: 12px;
		font-style: italic;
	}

	.rapp-widget-grid {
		display: grid;
		grid-template-columns: 1fr 1fr;
		gap: 4px;
		flex: 1;
		overflow: hidden;
	}

	.rapp-widget-thumb {
		width: 100%;
		aspect-ratio: 1;
		object-fit: cover;
		border-radius: 4px;
		background: var(--rs-bg-surface-raised);
	}

	/* Mode toggle button active state */
	.rapp-actions button.active {
		opacity: 1;
		background: rgba(0, 0, 0, 0.15);
	}
`;

declare global {
	interface HTMLElementTagNameMap {
		"folk-rapp": FolkRApp;
	}
}

// Module-specific port descriptors: meaningful typed ports per rApp module
const MODULE_PORTS: Record<string, PortDescriptor[]> = {
	rcal:     [{ name: "events-out", type: "json", direction: "output" },
	           { name: "upcoming-out", type: "json", direction: "output" },
	           { name: "new-event", type: "json", direction: "input" },
	           { name: "event-trigger", type: "trigger", direction: "input" }],
	rtasks:   [{ name: "tasks-out", type: "json", direction: "output" },
	           { name: "task-created", type: "trigger", direction: "output" },
	           { name: "create-task", type: "json", direction: "input" },
	           { name: "status-filter", type: "string", direction: "input" }],
	rflows:   [{ name: "allocations-out", type: "json", direction: "output" },
	           { name: "balance-out", type: "number", direction: "output" },
	           { name: "deposit-trigger", type: "trigger", direction: "input" },
	           { name: "flow-data", type: "json", direction: "output" }],
	rchoices: [{ name: "results-out", type: "json", direction: "output" },
	           { name: "decision-out", type: "json", direction: "output" },
	           { name: "vote-complete", type: "trigger", direction: "output" }],
	rvote:    [{ name: "proposals-out", type: "json", direction: "output" },
	           { name: "decision-out", type: "json", direction: "output" },
	           { name: "vote-complete", type: "trigger", direction: "output" },
	           { name: "proposal-passed", type: "boolean", direction: "output" }],
	rnotes:   [{ name: "notes-out", type: "json", direction: "output" },
	           { name: "create-note", type: "json", direction: "input" },
	           { name: "note-created", type: "trigger", direction: "output" }],
	rforum:   [{ name: "posts-out", type: "json", direction: "output" },
	           { name: "new-post", type: "trigger", direction: "output" },
	           { name: "thread-data", type: "json", direction: "output" }],
	rnetwork: [{ name: "graph-out", type: "json", direction: "output" },
	           { name: "nodes-out", type: "json", direction: "output" }],
	rinbox:   [{ name: "threads-out", type: "json", direction: "output" },
	           { name: "new-message", type: "trigger", direction: "output" }],
	rwallet:  [{ name: "balance-out", type: "number", direction: "output" },
	           { name: "transfer-trigger", type: "trigger", direction: "input" },
	           { name: "transfer-data", type: "json", direction: "input" }],
	rsocials: [{ name: "threads-out", type: "json", direction: "output" },
	           { name: "campaigns-out", type: "json", direction: "output" },
	           { name: "post-published", type: "trigger", direction: "output" },
	           { name: "campaign-data", type: "json", direction: "output" }],
};

const DEFAULT_PORTS: PortDescriptor[] = [
	{ name: "data-in", type: "json", direction: "input" },
	{ name: "data-out", type: "json", direction: "output" },
	{ name: "trigger-in", type: "trigger", direction: "input" },
	{ name: "trigger-out", type: "trigger", direction: "output" },
];

// API endpoint config per module for widget mode
const WIDGET_API: Record<string, { path: string; transform: (data: any) => WidgetData }> = {
	rinbox: {
		path: "/api/mailboxes",
		transform: (data) => {
			const mailboxes = data?.mailboxes || [];
			const totalThreads = mailboxes.reduce((sum: number, m: any) => sum + (m.threadCount || 0), 0);
			return {
				stat: `${totalThreads} thread${totalThreads !== 1 ? "s" : ""}`,
				rows: mailboxes.slice(0, 3).map((m: any) => ({
					label: m.name || m.slug,
					value: `${m.threadCount || 0}`,
				})),
			};
		},
	},
	rphotos: {
		path: "/api/assets",
		transform: (data) => {
			const assets = data?.assets || [];
			return {
				stat: `${assets.length} photo${assets.length !== 1 ? "s" : ""}`,
				thumbs: assets.slice(0, 4).map((a: any) => a.id),
			};
		},
	},
	rcal: {
		path: "/api/events?upcoming=true&limit=3",
		transform: (data) => {
			const events = data?.results || [];
			return {
				stat: `${data?.count ?? events.length} event${(data?.count ?? events.length) !== 1 ? "s" : ""}`,
				rows: events.slice(0, 3).map((ev: any) => ({
					label: ev.title || ev.summary || "Untitled",
					value: ev.start ? new Date(ev.start).toLocaleDateString(undefined, { month: "short", day: "numeric" }) : "",
				})),
			};
		},
	},
	rtasks: {
		path: "/api/spaces",
		transform: (data) => {
			const spaces = Array.isArray(data) ? data : [];
			const totalTasks = spaces.reduce((sum: number, s: any) => sum + (s.taskCount || 0), 0);
			return {
				stat: `${totalTasks} task${totalTasks !== 1 ? "s" : ""}`,
				rows: spaces.slice(0, 3).map((s: any) => ({
					label: s.name || s.slug,
					value: `${s.taskCount || 0}`,
				})),
			};
		},
	},
	rnotes: {
		path: "/api/notebooks",
		transform: (data) => {
			const notebooks = data?.notebooks || [];
			return {
				stat: `${notebooks.length} notebook${notebooks.length !== 1 ? "s" : ""}`,
				rows: notebooks.slice(0, 3).map((n: any) => ({
					label: n.title || n.name || "Untitled",
				})),
			};
		},
	},
	rflows: {
		path: "/api/flows",
		transform: (data) => {
			const flows = Array.isArray(data) ? data : [];
			const active = flows.filter((f: any) => f.status === "active").length;
			return {
				stat: `${active} active flow${active !== 1 ? "s" : ""}`,
				rows: flows.slice(0, 3).map((f: any) => ({
					label: f.name || f.id,
					value: f.status || "",
				})),
			};
		},
	},
	rschedule: {
		path: "/api/jobs",
		transform: (data) => {
			const jobs = data?.results || [];
			return {
				stat: `${data?.count ?? jobs.length} job${(data?.count ?? jobs.length) !== 1 ? "s" : ""}`,
				rows: jobs.slice(0, 3).map((j: any) => ({
					label: j.name || j.id,
					value: j.enabled === false ? "paused" : "active",
				})),
			};
		},
	},
	rsocials: {
		path: "/api/threads",
		transform: (data) => {
			const threads = data?.threads || [];
			const campaigns = data?.campaignCount ?? 0;
			return {
				stat: `${threads.length} thread${threads.length !== 1 ? "s" : ""}`,
				rows: threads.slice(0, 3).map((t: any) => ({
					label: t.title || "Untitled",
					value: `${t.tweets?.length || 0} tweets`,
				})),
			};
		},
	},
	rnetwork: {
		path: "/api/graph",
		transform: (data) => {
			const nodes = data?.nodes?.length ?? 0;
			const edges = data?.edges?.length ?? 0;
			return {
				stat: `${nodes} node${nodes !== 1 ? "s" : ""}`,
				rows: [
					{ label: "Nodes", value: `${nodes}` },
					{ label: "Edges", value: `${edges}` },
				],
			};
		},
	},
};

interface WidgetData {
	stat: string;
	rows?: { label: string; value?: string }[];
	thumbs?: string[];   // rphotos asset IDs for thumbnail grid
}

export class FolkRApp extends FolkShape {
	static override tagName = "folk-rapp";

	/** Enabled module IDs for picker/switcher filtering (null = all enabled) */
	static enabledModuleIds: Set<string> | null = null;

	/** Live instance registry for broadcasting enabled-state changes */
	static #instances = new Set<FolkRApp>();

	/** Update which modules appear in the picker and switcher dropdowns */
	static setEnabledModules(ids: string[] | null) {
		FolkRApp.enabledModuleIds = ids ? new Set(ids) : null;
		for (const inst of FolkRApp.#instances) inst.#syncDisabledState();
	}

	/** Port descriptors for data pipe integration (AC#3) */
	static override portDescriptors: PortDescriptor[] = [
		{ name: "data-in", type: "json", direction: "input" },
		{ name: "data-out", type: "json", direction: "output" },
		{ name: "trigger-in", type: "trigger", direction: "input" },
		{ name: "trigger-out", type: "trigger", direction: "output" },
	];

	static {
		const sheet = new CSSStyleSheet();
		const parentRules = Array.from(FolkShape.styles.cssRules)
			.map((r) => r.cssText)
			.join("\n");
		const childRules = Array.from(styles.cssRules)
			.map((r) => r.cssText)
			.join("\n");
		sheet.replaceSync(`${parentRules}\n${childRules}`);
		this.styles = sheet;
	}

	#moduleId: string = "";
	#spaceSlug: string = "";
	#mode: "widget" | "iframe" = "widget";
	#sandboxed: boolean = false;
	#ecosystemOrigin: string | null = null;
	#iframe: HTMLIFrameElement | null = null;
	#contentEl: HTMLElement | null = null;
	#messageHandler: ((e: MessageEvent) => void) | null = null;
	#statusEl: HTMLElement | null = null;
	#refreshTimer: ReturnType<typeof setInterval> | null = null;
	#modeToggleBtn: HTMLButtonElement | null = null;
	#resolvedPorts: PortDescriptor[] | null = null;
	#contentLoaded = false;

	get moduleId() { return this.#moduleId; }
	set moduleId(value: string) {
		if (this.#moduleId === value) return;
		this.#moduleId = value;
		this.#resolvedPorts = null;
		this.initPorts();
		this.requestUpdate("moduleId");
		this.dispatchEvent(new CustomEvent("content-change"));
		this.#renderContent();
	}

	get spaceSlug() { return this.#spaceSlug; }
	set spaceSlug(value: string) {
		if (this.#spaceSlug === value) return;
		this.#spaceSlug = value;
		this.requestUpdate("spaceSlug");
		this.dispatchEvent(new CustomEvent("content-change"));
		this.#renderContent();
	}

	get mode() { return this.#mode; }
	set mode(value: "widget" | "iframe") {
		if (this.#mode === value) return;
		this.#mode = value;
		this.dispatchEvent(new CustomEvent("content-change"));
		this.#updateModeToggle();
		this.#renderContent();
	}

	/** Resolve module-specific ports, falling back to generic defaults. */
	#getModulePorts(): PortDescriptor[] {
		if (this.#resolvedPorts) return this.#resolvedPorts;
		this.#resolvedPorts = MODULE_PORTS[this.#moduleId] || DEFAULT_PORTS;
		return this.#resolvedPorts;
	}

	override getPort(name: string): PortDescriptor | undefined {
		return this.#getModulePorts().find(p => p.name === name);
	}

	override getInputPorts(): PortDescriptor[] {
		return this.#getModulePorts().filter(p => p.direction === "input");
	}

	override getOutputPorts(): PortDescriptor[] {
		return this.#getModulePorts().filter(p => p.direction === "output");
	}

	override initPorts(): void {
		const descriptors = this.#getModulePorts();
		for (const desc of descriptors) {
			if (this.getPortValue(desc.name) === undefined) {
				this.setPortValueSilent(desc.name, undefined);
			}
		}
	}

	override createRenderRoot() {
		const root = super.createRenderRoot();

		// Initialize typed data ports (AC#3)
		this.initPorts();

		// Prefer JS-set properties (from newShape props); fall back to HTML attributes
		if (!this.#moduleId) this.#moduleId = this.getAttribute("module-id") || "";
		if (!this.#spaceSlug) this.#spaceSlug = this.getAttribute("space-slug") || "";
		const attrSandboxed = this.getAttribute("sandboxed");
		if (attrSandboxed === "true" || attrSandboxed === "") this.#sandboxed = true;
		this.#ecosystemOrigin = this.getAttribute("ecosystem-origin") || null;
		const attrMode = this.getAttribute("mode");
		if (attrMode === "iframe" || attrMode === "widget") this.#mode = attrMode;

		// Auto-derive spaceSlug from current URL if not explicitly provided (/{space}/rspace → space)
		if (!this.#spaceSlug) {
			const pathParts = window.location.pathname.split("/").filter(Boolean);
			if (pathParts.length >= 1) this.#spaceSlug = pathParts[0];
		}

		const meta = MODULE_META[this.#moduleId];
		const headerColor = meta?.color || "#475569";
		const headerName = meta?.name || this.#moduleId || "rApp";
		const headerBadge = meta?.badge || "r?";
		const headerIcon = meta?.icon || "📱";

		const wrapper = document.createElement("div");
		wrapper.className = "rapp-wrapper";
		wrapper.innerHTML = html`
			<div class="header rapp-header" data-drag style="--rapp-color: ${headerColor}">
				<div class="rapp-header-left">
					<span class="rapp-badge">${headerBadge}</span>
					<span class="rapp-name">${headerName}</span>
					<span class="rapp-icon">${headerIcon}</span>
					<span class="rapp-status" title="Not connected"></span>
					<div class="rapp-switcher"></div>
				</div>
				<div class="rapp-actions">
					<button class="mode-toggle-btn" title="Toggle widget/iframe mode">${this.#mode === "widget" ? "□" : "▪"}</button>
					<button class="switch-btn" title="Switch module">⇄</button>
					<button class="open-tab-btn" title="Open in tab">↗</button>
					<button class="close-btn" title="Close">×</button>
				</div>
			</div>
			<div class="rapp-content"></div>
		`;

		const slot = root.querySelector("slot");
		const containerDiv = slot?.parentElement as HTMLElement;
		if (containerDiv) {
			containerDiv.replaceWith(wrapper);
		}

		this.#contentEl = wrapper.querySelector(".rapp-content") as HTMLElement;
		this.#statusEl = wrapper.querySelector(".rapp-status") as HTMLElement;
		this.#modeToggleBtn = wrapper.querySelector(".mode-toggle-btn") as HTMLButtonElement;
		const switchBtn = wrapper.querySelector(".switch-btn") as HTMLButtonElement;
		const openTabBtn = wrapper.querySelector(".open-tab-btn") as HTMLButtonElement;
		const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement;
		const switcherEl = wrapper.querySelector(".rapp-switcher") as HTMLElement;

		// Mode toggle: widget ↔ iframe
		this.#modeToggleBtn.addEventListener("click", (e) => {
			e.stopPropagation();
			this.mode = this.#mode === "widget" ? "iframe" : "widget";
		});

		// Module switcher dropdown
		this.#buildSwitcher(switcherEl);
		switchBtn.addEventListener("click", (e) => {
			e.stopPropagation();
			switcherEl.classList.toggle("open");
		});

		// Close switcher when clicking elsewhere
		const closeSwitcher = () => switcherEl.classList.remove("open");
		root.addEventListener("click", closeSwitcher);

		// Open in tab — navigate to the module's page via tab bar
		openTabBtn.addEventListener("click", (e) => {
			e.stopPropagation();
			if (this.#moduleId && this.#spaceSlug) {
				window.location.href = rspaceNavUrl(this.#spaceSlug, this.#moduleId);
			}
		});

		// Close button
		closeBtn.addEventListener("click", (e) => {
			e.stopPropagation();
			this.dispatchEvent(new CustomEvent("close"));
		});

		// Set up postMessage listener
		this.#messageHandler = (e: MessageEvent) => this.#handleMessage(e);
		window.addEventListener("message", this.#messageHandler);

		// Forward input port data to iframe (AC#3)
		this.addEventListener("port-value-changed", ((e: CustomEvent) => {
			const { name, value } = e.detail;
			const port = this.getPort(name);
			if (!port || port.direction !== "input" || !this.#iframe?.contentWindow) return;

			const targetOrigin = this.#sandboxed && this.#ecosystemOrigin
				? this.#ecosystemOrigin : window.location.origin;
			try {
				if (port.type === "trigger") {
					this.#iframe.contentWindow.postMessage({
						source: "rspace-parent", type: "trigger",
						portName: name,
					}, targetOrigin);
				} else {
					this.#iframe.contentWindow.postMessage({
						source: "rspace-parent", type: "port-data",
						portName: name, value,
					}, targetOrigin);
				}
			} catch { /* iframe not ready */ }
		}) as EventListener);

		// Register for enabled-state broadcasts
		FolkRApp.#instances.add(this);

		// Defer heavy content loading until shape enters viewport
		if (this.#moduleId) {
			if (this.hasBeenVisible) {
				this.#renderContent();
			} else {
				this.#showLoading();
			}
		} else {
			this.#showPicker();
		}

		return root;
	}

	override disconnectedCallback() {
		super.disconnectedCallback();
		FolkRApp.#instances.delete(this);
		if (this.#messageHandler) {
			window.removeEventListener("message", this.#messageHandler);
			this.#messageHandler = null;
		}
		if (this.#refreshTimer) {
			clearInterval(this.#refreshTimer);
			this.#refreshTimer = null;
		}
	}

	protected override onBecameVisible(): void {
		if (this.#moduleId && !this.#contentLoaded) {
			this.#renderContent();
		}
	}

	#showLoading() {
		if (!this.#contentEl) return;
		const meta = MODULE_META[this.#moduleId];
		this.#contentEl.innerHTML = `
			<div class="rapp-loading">
				<div class="spinner"></div>
				<span>${meta?.name || this.#moduleId}</span>
			</div>
		`;
	}

	#buildSwitcher(switcherEl: HTMLElement) {
		const enabledSet = FolkRApp.enabledModuleIds
			?? ((window as any).__rspaceEnabledModules ? new Set((window as any).__rspaceEnabledModules as string[]) : null);

		const items = Object.entries(MODULE_META)
			.filter(([id]) => !enabledSet || enabledSet.has(id))
			.map(([id, meta]) => `
				<button class="rapp-switcher-item ${id === this.#moduleId ? "active" : ""}" data-module="${id}">
					<span class="rapp-switcher-badge" style="background: ${meta.color}">${meta.badge}</span>
					<span>${meta.name} ${meta.icon}</span>
				</button>
			`)
			.join("");

		switcherEl.innerHTML = items;

		switcherEl.querySelectorAll(".rapp-switcher-item").forEach((btn) => {
			btn.addEventListener("click", (e) => {
				e.stopPropagation();
				const modId = (btn as HTMLElement).dataset.module;
				if (modId && modId !== this.#moduleId) {
					this.moduleId = modId;
					this.#buildSwitcher(switcherEl);
				}
				switcherEl.classList.remove("open");
			});
		});
	}

	/** Handle postMessage from embedded iframe */
	#handleMessage(e: MessageEvent) {
		if (!this.#iframe) return;

		// Only accept messages from our iframe
		if (e.source !== this.#iframe.contentWindow) return;

		// Always validate origin — defense-in-depth for both sandboxed and internal
		const expectedOrigin = (this.#sandboxed && this.#ecosystemOrigin)
			? this.#ecosystemOrigin
			: window.location.origin;
		if (e.origin !== expectedOrigin) {
			console.warn(`[folk-rapp] Rejected message from ${e.origin}, expected ${expectedOrigin}`);
			return;
		}

		const msg = e.data;
		if (!msg || typeof msg !== "object") return;

		// CommunitySync shape updates from the embedded module
		if (msg.source === "rspace-canvas" && msg.type === "shape-updated") {
			this.dispatchEvent(new CustomEvent("rapp-data", {
				detail: { moduleId: this.#moduleId, shapeId: msg.shapeId, data: msg.data },
				bubbles: true,
			}));

			// Forward shape data to output port (AC#3)
			this.setPortValue("data-out", msg.data);

			// Mark as connected
			if (this.#statusEl) {
				this.#statusEl.classList.add("connected");
				this.#statusEl.title = "Connected — receiving data";
			}
		}

		// Navigation request from embedded module
		if (msg.source === "rspace-rapp" && msg.type === "navigate" && msg.moduleId) {
			this.moduleId = msg.moduleId;
		}

		// Port update from sandboxed embed (AC#4)
		if (msg.source === "rspace-ecosystem-embed" && msg.type === "port-update") {
			if (msg.portName && msg.value !== undefined) {
				this.setPortValue(msg.portName, msg.value);
			}
		}

		// Event emit from sandboxed embed (AC#4)
		if (msg.source === "rspace-ecosystem-embed" && msg.type === "event-emit") {
			if (msg.channel) {
				this.dispatchEvent(new CustomEvent("ecosystem-event", {
					detail: { channel: msg.channel, payload: msg.payload, shapeId: this.id },
					bubbles: true,
				}));
			}
		}
	}

	/** Send context to the iframe after it loads */
	#sendContext() {
		if (!this.#iframe?.contentWindow) return;
		// Use origin-validated postMessage for sandboxed ecosystem embeds
		const targetOrigin = this.#sandboxed && this.#ecosystemOrigin
			? this.#ecosystemOrigin
			: window.location.origin; // same-origin for internal rApps
		try {
			this.#iframe.contentWindow.postMessage({
				source: "rspace-parent",
				type: "context",
				shapeId: this.id,
				space: this.#spaceSlug,
				moduleId: this.#moduleId,
				embedded: true,
				sandboxed: this.#sandboxed,
			}, targetOrigin);
		} catch {
			// cross-origin or iframe not ready
		}
	}

	#updateModeToggle() {
		if (this.#modeToggleBtn) {
			this.#modeToggleBtn.textContent = this.#mode === "widget" ? "□" : "▪";
			this.#modeToggleBtn.title = this.#mode === "widget" ? "Expand to iframe" : "Collapse to widget";
		}
	}

	/** Derive the base module URL path — spaces are always subdomains */
	#getModulePath(): string {
		return `/${this.#moduleId}`;
	}

	/** Check if this shape's module is currently disabled */
	#isModuleDisabled(): boolean {
		const enabled = FolkRApp.enabledModuleIds;
		if (!enabled) return false; // null = all enabled
		return this.#moduleId !== "" && !enabled.has(this.#moduleId);
	}

	/** Show the disabled overlay and tear down any active iframe/timer */
	#showDisabled() {
		if (!this.#contentEl) return;

		// Clear iframe and timer
		this.#iframe = null;
		if (this.#refreshTimer) {
			clearInterval(this.#refreshTimer);
			this.#refreshTimer = null;
		}

		this.setAttribute("data-module-disabled", "");

		const meta = MODULE_META[this.#moduleId];
		this.#contentEl.innerHTML = `
			<div class="rapp-disabled">
				<span class="rapp-disabled-icon">🔒</span>
				<span>${meta?.name || this.#moduleId} is disabled</span>
			</div>
		`;
	}

	/** Sync this instance's visual state with the current enabled set */
	#syncDisabledState() {
		if (!this.#contentEl || !this.#moduleId) return;
		const disabled = this.#isModuleDisabled();
		const wasDisabled = this.hasAttribute("data-module-disabled");

		if (disabled && !wasDisabled) {
			this.#showDisabled();
		} else if (!disabled && wasDisabled) {
			this.removeAttribute("data-module-disabled");
			this.#renderContent();
		}
	}

	/** Route to the right render method based on current mode */
	#renderContent() {
		if (!this.#contentEl || !this.#moduleId) return;

		this.#contentLoaded = true;

		// Block rendering if module is disabled
		if (this.#isModuleDisabled()) { this.#showDisabled(); return; }

		// Clear refresh timer
		if (this.#refreshTimer) {
			clearInterval(this.#refreshTimer);
			this.#refreshTimer = null;
		}

		// Update header
		this.#updateHeader();

		if (this.#mode === "widget") {
			this.#loadWidget();
		} else {
			this.#loadIframe();
		}
	}

	#updateHeader() {
		const meta = MODULE_META[this.#moduleId];
		const header = this.shadowRoot?.querySelector(".rapp-header") as HTMLElement;
		if (header && meta) {
			header.style.setProperty("--rapp-color", meta.color);
			const badge = header.querySelector(".rapp-badge");
			const name = header.querySelector(".rapp-name");
			const icon = header.querySelector(".rapp-icon");
			if (badge) badge.textContent = meta.badge;
			if (name) name.textContent = meta.name;
			if (icon) icon.textContent = meta.icon;
		}
	}

	/** Iframe mode: load the full rApp in an iframe with overlay for event capture */
	#loadIframe() {
		if (!this.#contentEl) return;

		// Reset connection status
		if (this.#statusEl) {
			this.#statusEl.classList.remove("connected");
			this.#statusEl.title = "Loading...";
		}

		const meta = MODULE_META[this.#moduleId];

		// Show loading state
		this.#contentEl.innerHTML = `
			<div class="rapp-overlay"></div>
			<div class="rapp-loading">
				<div class="spinner"></div>
				<span>Loading ${meta?.name || this.#moduleId}...</span>
			</div>
		`;

		const iframeUrl = this.#getModulePath();

		const iframe = document.createElement("iframe");
		iframe.className = "rapp-iframe";
		iframe.src = iframeUrl;
		iframe.loading = "lazy";
		iframe.allow = "clipboard-write";

		// Sandboxed mode for untrusted ecosystem apps (AC#4)
		// NEVER combine allow-scripts + allow-same-origin — that lets the
		// iframe remove its own sandbox attribute (spec-documented escape).
		if (this.#sandboxed) {
			iframe.sandbox.add("allow-scripts");
			iframe.sandbox.add("allow-forms");
		}

		iframe.addEventListener("load", () => {
			const loading = this.#contentEl?.querySelector(".rapp-loading");
			if (loading) loading.remove();
			this.#sendContext();
		});

		iframe.addEventListener("error", () => {
			if (this.#contentEl) {
				this.#contentEl.innerHTML = `
					<div class="rapp-overlay"></div>
					<div class="rapp-error">
						<span>Failed to load ${meta?.name || this.#moduleId}</span>
						<button class="rapp-picker-item" style="justify-content: center; color: #94a3b8;">
							Retry
						</button>
					</div>
				`;
				this.#contentEl.querySelector("button")?.addEventListener("click", () => this.#loadIframe());
			}
		});

		this.#contentEl.appendChild(iframe);
		this.#iframe = iframe;
	}

	/** Widget mode: fetch data from module API and render a compact card */
	#loadWidget() {
		if (!this.#contentEl) return;

		this.#iframe = null;

		// Reset status — widget mode doesn't use postMessage connection
		if (this.#statusEl) {
			this.#statusEl.classList.remove("connected");
			this.#statusEl.title = "Widget mode";
		}

		const meta = MODULE_META[this.#moduleId];

		// Show loading
		this.#contentEl.innerHTML = `
			<div class="rapp-loading">
				<div class="spinner"></div>
				<span>Loading ${meta?.name || this.#moduleId}...</span>
			</div>
		`;

		// Fetch and render
		this.#fetchAndRenderWidget();

		// Auto-refresh every 60 seconds
		this.#refreshTimer = setInterval(() => this.#fetchAndRenderWidget(), 60_000);
	}

	async #fetchAndRenderWidget() {
		if (!this.#contentEl || this.#mode !== "widget") return;

		const apiConfig = WIDGET_API[this.#moduleId];
		if (!apiConfig) {
			this.#renderWidgetFallback();
			return;
		}

		const basePath = this.#getModulePath();
		const url = `${basePath}${apiConfig.path}`;

		try {
			const res = await fetch(url);
			if (!res.ok) throw new Error(`${res.status}`);
			const data = await res.json();
			const widgetData = apiConfig.transform(data);
			this.#renderWidgetCard(widgetData);

			// Emit raw API data on the first json output port
			const outputPorts = this.getOutputPorts();
			const jsonOut = outputPorts.find(p => p.type === "json");
			if (jsonOut) {
				this.setPortValue(jsonOut.name, data);
			}
		} catch {
			this.#renderWidgetFallback();
		}
	}

	#renderWidgetCard(data: WidgetData) {
		if (!this.#contentEl) return;

		const meta = MODULE_META[this.#moduleId];
		const basePath = this.#getModulePath();

		let listHtml = "";

		if (data.thumbs && data.thumbs.length > 0) {
			// Photo grid mode
			const thumbItems = data.thumbs
				.map((id) => `<img class="rapp-widget-thumb" src="${basePath}/api/assets/${id}/thumbnail" alt="" loading="lazy" />`)
				.join("");
			listHtml = `<div class="rapp-widget-grid">${thumbItems}</div>`;
		} else if (data.rows && data.rows.length > 0) {
			const rowItems = data.rows
				.map((r) => `
					<div class="rapp-widget-row">
						<span class="bullet">•</span>
						<span class="label">${this.#escapeHtml(r.label)}</span>
						${r.value ? `<span class="value">${this.#escapeHtml(r.value)}</span>` : ""}
					</div>
				`)
				.join("");
			listHtml = `
				<hr class="rapp-widget-divider" />
				<div class="rapp-widget-list">${rowItems}</div>
			`;
		} else {
			listHtml = `<span class="rapp-widget-empty">No data yet</span>`;
		}

		this.#contentEl.innerHTML = `
			<div class="rapp-widget" title="Click to open ${meta?.name || this.#moduleId}">
				<div class="rapp-widget-stat">${this.#escapeHtml(data.stat)}</div>
				${listHtml}
			</div>
		`;

		// Click widget → navigate to full module
		this.#contentEl.querySelector(".rapp-widget")?.addEventListener("click", () => {
			if (this.#moduleId && this.#spaceSlug) {
				window.location.href = rspaceNavUrl(this.#spaceSlug, this.#moduleId);
			}
		});
	}

	/** Fallback widget when no API config exists or fetch fails */
	#renderWidgetFallback() {
		if (!this.#contentEl) return;

		const meta = MODULE_META[this.#moduleId];
		this.#contentEl.innerHTML = `
			<div class="rapp-widget" title="Click to open ${meta?.name || this.#moduleId}">
				<div class="rapp-widget-stat">${meta?.icon || "📱"} ${meta?.name || this.#moduleId}</div>
				<span class="rapp-widget-empty">Could not load data</span>
			</div>
		`;

		this.#contentEl.querySelector(".rapp-widget")?.addEventListener("click", () => {
			if (this.#moduleId && this.#spaceSlug) {
				window.location.href = rspaceNavUrl(this.#spaceSlug, this.#moduleId);
			}
		});
	}

	#escapeHtml(str: string): string {
		const div = document.createElement("div");
		div.textContent = str;
		return div.innerHTML;
	}

	#showPicker() {
		if (!this.#contentEl) return;

		const enabledSet = FolkRApp.enabledModuleIds
			?? ((window as any).__rspaceEnabledModules ? new Set((window as any).__rspaceEnabledModules as string[]) : null);

		const items = Object.entries(MODULE_META)
			.filter(([id]) => !enabledSet || enabledSet.has(id))
			.map(([id, meta]) => `
				<button class="rapp-picker-item" data-module="${id}">
					<span class="rapp-picker-badge" style="background: ${meta.color}">${meta.badge}</span>
					<span>${meta.name}</span>
					<span>${meta.icon}</span>
				</button>
			`)
			.join("");

		this.#contentEl.innerHTML = `
			<div class="rapp-picker">
				<span class="rapp-picker-title">Choose an rApp to embed</span>
				${items}
			</div>
		`;

		this.#contentEl.querySelectorAll(".rapp-picker-item").forEach((btn) => {
			btn.addEventListener("click", () => {
				const modId = (btn as HTMLElement).dataset.module;
				if (modId) {
					this.moduleId = modId;
				}
			});
		});
	}

	/**
	 * Handle event bus broadcasts (AC#3).
	 * Forward events to the iframe via postMessage so embedded modules can react.
	 */
	override onEventReceived(channel: string, payload: unknown, sourceShapeId: string): void {
		if (!this.#iframe?.contentWindow) return;
		const targetOrigin = this.#sandboxed && this.#ecosystemOrigin
			? this.#ecosystemOrigin
			: window.location.origin;
		try {
			this.#iframe.contentWindow.postMessage({
				source: "rspace-parent",
				type: "event",
				channel,
				payload,
				sourceShapeId,
			}, targetOrigin);
		} catch {
			// cross-origin or iframe not ready
		}
	}

	static override fromData(data: Record<string, any>): FolkRApp {
		const shape = FolkShape.fromData(data) as FolkRApp;
		if (data.moduleId !== undefined) shape.moduleId = data.moduleId;
		if (data.spaceSlug !== undefined) shape.spaceSlug = data.spaceSlug;
		if (data.mode === "widget" || data.mode === "iframe") shape.mode = data.mode;
		if (data.sandboxed) shape.#sandboxed = true;
		if (data.ecosystemOrigin) shape.#ecosystemOrigin = data.ecosystemOrigin;
		return shape;
	}

	override applyData(data: Record<string, any>): void {
		super.applyData(data);
		if (data.moduleId !== undefined && data.moduleId !== this.moduleId) this.moduleId = data.moduleId;
		if (data.spaceSlug !== undefined && data.spaceSlug !== this.spaceSlug) this.spaceSlug = data.spaceSlug;
		if ((data.mode === "widget" || data.mode === "iframe") && data.mode !== this.mode) this.mode = data.mode;
		if (data.sandboxed !== undefined) this.#sandboxed = !!data.sandboxed;
		if (data.ecosystemOrigin !== undefined) this.#ecosystemOrigin = data.ecosystemOrigin;
	}

	override toJSON() {
		return {
			...super.toJSON(),
			type: "folk-rapp",
			moduleId: this.#moduleId,
			spaceSlug: this.#spaceSlug,
			mode: this.#mode,
			sandboxed: this.#sandboxed,
			ecosystemOrigin: this.#ecosystemOrigin,
		};
	}
}
