/**
 * DemoSync — vanilla JS class for real-time demo data via rSpace WebSocket.
 *
 * Mirrors the React useDemoSync hook but uses EventTarget for framework-free
 * operation. Designed for module demo pages served as server-rendered HTML.
 *
 * Events:
 *   - "snapshot"     → detail: { shapes: Record<string, DemoShape> }
 *   - "connected"    → WebSocket opened
 *   - "disconnected" → WebSocket closed
 *
 * Usage:
 *   const sync = new DemoSync({ filter: ['demo-poll'] });
 *   sync.addEventListener('snapshot', (e) => renderUI(e.detail.shapes));
 *   sync.connect();
 */

export interface DemoShape {
	type: string;
	id: string;
	x: number;
	y: number;
	width: number;
	height: number;
	rotation: number;
	[key: string]: unknown;
}

export interface DemoSyncOptions {
	/** Community slug (default: 'demo') */
	slug?: string;
	/** Only subscribe to these shape types */
	filter?: string[];
	/** rSpace server URL (default: auto-detect) */
	serverUrl?: string;
}

const DEFAULT_SLUG = "demo";
const RECONNECT_BASE_MS = 1000;
const RECONNECT_MAX_MS = 30000;
const PING_INTERVAL_MS = 30000;

function getDefaultServerUrl(): string {
	if (typeof window === "undefined") return "https://rspace.online";
	const host = window.location.hostname;
	if (host === "localhost" || host === "127.0.0.1") {
		return `http://${host}:3000`;
	}
	return "https://rspace.online";
}

export class DemoSync extends EventTarget {
	private slug: string;
	private filter: string[] | undefined;
	private serverUrl: string;
	private ws: WebSocket | null = null;
	private reconnectAttempt = 0;
	private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
	private pingTimer: ReturnType<typeof setInterval> | null = null;
	private destroyed = false;

	shapes: Record<string, DemoShape> = {};
	connected = false;

	constructor(options?: DemoSyncOptions) {
		super();
		this.slug = options?.slug ?? DEFAULT_SLUG;
		this.filter = options?.filter;
		this.serverUrl = options?.serverUrl ?? getDefaultServerUrl();
	}

	connect(): void {
		if (this.destroyed) return;

		const wsProtocol = this.serverUrl.startsWith("https") ? "wss" : "ws";
		const host = this.serverUrl.replace(/^https?:\/\//, "");
		const wsUrl = `${wsProtocol}://${host}/ws/${this.slug}?mode=json`;

		const ws = new WebSocket(wsUrl);
		this.ws = ws;

		ws.onopen = () => {
			if (this.destroyed) return;
			this.connected = true;
			this.reconnectAttempt = 0;
			this.dispatchEvent(new Event("connected"));

			this.pingTimer = setInterval(() => {
				if (ws.readyState === WebSocket.OPEN) {
					ws.send(JSON.stringify({ type: "ping", timestamp: Date.now() }));
				}
			}, PING_INTERVAL_MS);
		};

		ws.onmessage = (event) => {
			if (this.destroyed) return;
			try {
				const msg = JSON.parse(event.data);
				if (msg.type === "snapshot" && msg.shapes) {
					this.shapes = this.applyFilter(msg.shapes);
					this.dispatchEvent(
						new CustomEvent("snapshot", {
							detail: { shapes: this.shapes },
						}),
					);
				}
			} catch {
				// ignore parse errors
			}
		};

		ws.onclose = () => {
			if (this.destroyed) return;
			this.connected = false;
			this.cleanup();
			this.dispatchEvent(new Event("disconnected"));
			this.scheduleReconnect();
		};

		ws.onerror = () => {
			// onclose fires after onerror
		};
	}

	updateShape(id: string, data: Partial<DemoShape>): void {
		const ws = this.ws;
		if (!ws || ws.readyState !== WebSocket.OPEN) return;

		// Optimistic local update
		const existing = this.shapes[id];
		if (existing) {
			const updated = { ...existing, ...data, id };
			if (this.filter && this.filter.length > 0 && !this.filter.includes(updated.type)) return;
			this.shapes = { ...this.shapes, [id]: updated as DemoShape };
			this.dispatchEvent(
				new CustomEvent("snapshot", {
					detail: { shapes: this.shapes },
				}),
			);
		}

		ws.send(JSON.stringify({ type: "update", id, data: { ...data, id } }));
	}

	deleteShape(id: string): void {
		const ws = this.ws;
		if (!ws || ws.readyState !== WebSocket.OPEN) return;

		const { [id]: _, ...rest } = this.shapes;
		this.shapes = rest;
		this.dispatchEvent(
			new CustomEvent("snapshot", { detail: { shapes: this.shapes } }),
		);
		ws.send(JSON.stringify({ type: "delete", id }));
	}

	async resetDemo(): Promise<void> {
		const res = await fetch(`${this.serverUrl}/api/communities/demo/reset`, {
			method: "POST",
		});
		if (!res.ok) {
			const body = await res.text();
			throw new Error(`Reset failed: ${res.status} ${body}`);
		}
	}

	destroy(): void {
		this.destroyed = true;
		this.cleanup();
		if (this.reconnectTimer) {
			clearTimeout(this.reconnectTimer);
			this.reconnectTimer = null;
		}
		if (this.ws) {
			this.ws.onclose = null;
			this.ws.close();
			this.ws = null;
		}
	}

	private applyFilter(allShapes: Record<string, DemoShape>): Record<string, DemoShape> {
		if (!this.filter || this.filter.length === 0) return allShapes;
		const filtered: Record<string, DemoShape> = {};
		for (const [id, shape] of Object.entries(allShapes)) {
			if (this.filter.includes(shape.type)) {
				filtered[id] = shape;
			}
		}
		return filtered;
	}

	private cleanup(): void {
		if (this.pingTimer) {
			clearInterval(this.pingTimer);
			this.pingTimer = null;
		}
	}

	private scheduleReconnect(): void {
		if (this.destroyed) return;
		const delay = Math.min(
			RECONNECT_BASE_MS * Math.pow(2, this.reconnectAttempt),
			RECONNECT_MAX_MS,
		);
		this.reconnectAttempt++;
		this.reconnectTimer = setTimeout(() => {
			if (!this.destroyed) this.connect();
		}, delay);
	}
}
