/**
 * WebSocket-based room sync for rMaps.
 * Ported from rmaps-online/src/lib/sync.ts (simplified — no @/types dependency).
 */

// ── Typed unions ──────────────────────────────────────────────

export type ParticipantStatus = "online" | "away" | "ghost" | "offline";
export type WaypointType = "meeting" | "poi" | "parking" | "food" | "danger" | "custom";
export type LocationSource = "gps" | "network" | "manual" | "ip" | "indoor";
export type PrecisionLevel = "exact" | "building" | "area" | "approximate" | "hidden";

export interface PrivacySettings {
	precision: PrecisionLevel;
	ghostMode: boolean;
}

// ── State interfaces ──────────────────────────────────────────

export interface RoomState {
	id: string;
	slug: string;
	name: string;
	createdAt: string;
	participants: Record<string, ParticipantState>;
	waypoints: WaypointState[];
}

export interface ParticipantState {
	id: string;
	name: string;
	emoji: string;
	color: string;
	joinedAt: string;
	lastSeen: string;
	status: ParticipantStatus;
	location?: LocationState;
}

export interface LocationState {
	latitude: number;
	longitude: number;
	accuracy: number;
	altitude?: number;
	heading?: number;
	speed?: number;
	timestamp: string;
	source: LocationSource;
	indoor?: { level: number; x: number; y: number; spaceName?: string };
}

export interface WaypointState {
	id: string;
	name: string;
	emoji?: string;
	latitude: number;
	longitude: number;
	indoor?: { level: number; x: number; y: number };
	createdBy: string;
	createdAt: string;
	type: WaypointType;
}

export type SyncMessage =
	| { type: "join"; participant: ParticipantState }
	| { type: "leave"; participantId: string }
	| { type: "location"; participantId: string; location: LocationState }
	| { type: "status"; participantId: string; status: ParticipantStatus }
	| { type: "waypoint_add"; waypoint: WaypointState }
	| { type: "waypoint_remove"; waypointId: string }
	| { type: "full_state"; state: RoomState }
	| { type: "request_state" }
	| { type: "request_location"; manual?: boolean; callerName?: string }
	| { type: "route_request"; fromId: string; fromName: string; fromEmoji: string };

type SyncCallback = (state: RoomState) => void;
type ConnectionCallback = (connected: boolean) => void;
type LocationRequestCallback = (manual?: boolean, callerName?: string) => void;
type RouteRequestCallback = (fromId: string, fromName: string, fromEmoji: string) => void;

export function isValidLocation(location: LocationState | undefined): boolean {
	if (!location) return false;
	const { latitude, longitude } = location;
	if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) return false;
	if (latitude === 0 && longitude === 0) return false;
	if (isNaN(latitude) || isNaN(longitude)) return false;
	return true;
}

export class RoomSync {
	private slug: string;
	private state: RoomState;
	private ws: WebSocket | null = null;
	private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
	private onStateChange: SyncCallback;
	private onConnectionChange: ConnectionCallback;
	private onLocationRequest: LocationRequestCallback | null = null;
	private onRouteRequest: RouteRequestCallback | null = null;
	private participantId: string;
	private currentParticipant: ParticipantState | null = null;

	constructor(
		slug: string,
		participantId: string,
		onStateChange: SyncCallback,
		onConnectionChange: ConnectionCallback,
		onLocationRequest?: LocationRequestCallback,
		onRouteRequest?: RouteRequestCallback,
	) {
		this.slug = slug;
		this.participantId = participantId;
		this.onStateChange = onStateChange;
		this.onConnectionChange = onConnectionChange;
		this.onLocationRequest = onLocationRequest || null;
		this.onRouteRequest = onRouteRequest || null;
		this.state = this.loadState() || this.createInitialState();
	}

	private createInitialState(): RoomState {
		return {
			id: crypto.randomUUID(),
			slug: this.slug,
			name: this.slug,
			createdAt: new Date().toISOString(),
			participants: {},
			waypoints: [],
		};
	}

	private loadState(): RoomState | null {
		try {
			const stored = localStorage.getItem(`rmaps_room_${this.slug}`);
			if (stored) {
				const state = JSON.parse(stored) as RoomState;
				return this.cleanupStaleParticipants(state);
			}
		} catch (e) {
			console.warn("Failed to load room state:", e);
		}
		return null;
	}

	private cleanupStaleParticipants(state: RoomState): RoomState {
		const STALE_MS = 15 * 60 * 1000;
		const now = Date.now();
		const cleaned: Record<string, ParticipantState> = {};
		for (const [id, p] of Object.entries(state.participants)) {
			const isStale = now - new Date(p.lastSeen).getTime() > STALE_MS;
			if (id === this.participantId || !isStale) {
				if (p.location && !isValidLocation(p.location)) delete p.location;
				cleaned[id] = p;
			}
		}
		return { ...state, participants: cleaned };
	}

	private saveState(): void {
		try {
			localStorage.setItem(`rmaps_room_${this.slug}`, JSON.stringify(this.state));
		} catch {}
	}

	private notifyStateChange(): void {
		this.saveState();
		this.onStateChange({ ...this.state });
	}

	connect(syncUrl?: string): void {
		if (!syncUrl) {
			this.onConnectionChange(true);
			return;
		}
		try {
			this.ws = new WebSocket(`${syncUrl}/room/${this.slug}`);
			this.ws.onopen = () => {
				this.onConnectionChange(true);
				if (this.currentParticipant) {
					this.send({ type: "join", participant: this.currentParticipant });
				}
			};
			this.ws.onmessage = (event) => {
				try {
					this.handleMessage(JSON.parse(event.data));
				} catch {}
			};
			this.ws.onclose = () => {
				this.onConnectionChange(false);
				this.scheduleReconnect(syncUrl);
			};
			this.ws.onerror = () => {};
		} catch {
			this.onConnectionChange(false);
		}
	}

	private scheduleReconnect(syncUrl: string): void {
		if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
		this.reconnectTimer = setTimeout(() => this.connect(syncUrl), 5000);
	}

	private send(message: SyncMessage): void {
		if (this.ws?.readyState === WebSocket.OPEN) {
			this.ws.send(JSON.stringify(message));
		}
	}

	private handleMessage(message: SyncMessage): void {
		switch (message.type) {
			case "full_state": {
				const myP = this.state.participants[this.participantId];
				this.state = message.state;
				if (myP) this.state.participants[this.participantId] = myP;
				break;
			}
			case "join":
				this.state.participants[message.participant.id] = message.participant;
				break;
			case "leave":
				delete this.state.participants[message.participantId];
				break;
			case "location":
				if (this.state.participants[message.participantId]) {
					if (message.location && isValidLocation(message.location)) {
						this.state.participants[message.participantId].location = message.location;
						this.state.participants[message.participantId].lastSeen = new Date().toISOString();
					} else if (message.location === null) {
						delete this.state.participants[message.participantId].location;
						this.state.participants[message.participantId].lastSeen = new Date().toISOString();
					}
				}
				break;
			case "status":
				if (this.state.participants[message.participantId]) {
					this.state.participants[message.participantId].status = message.status;
					this.state.participants[message.participantId].lastSeen = new Date().toISOString();
				}
				break;
			case "waypoint_add":
				this.state.waypoints.push(message.waypoint);
				break;
			case "waypoint_remove":
				this.state.waypoints = this.state.waypoints.filter((w) => w.id !== message.waypointId);
				break;
			case "request_location":
				if (this.onLocationRequest) this.onLocationRequest(message.manual, message.callerName);
				return;
			case "route_request":
				if (message.fromId !== this.participantId && this.onRouteRequest) {
					this.onRouteRequest(message.fromId, message.fromName, message.fromEmoji);
				}
				return;
		}
		this.notifyStateChange();
	}

	join(participant: ParticipantState): void {
		this.currentParticipant = participant;
		this.state.participants[participant.id] = participant;
		this.send({ type: "join", participant });
		this.notifyStateChange();
	}

	leave(): void {
		delete this.state.participants[this.participantId];
		this.send({ type: "leave", participantId: this.participantId });
		this.notifyStateChange();
		if (this.ws) { this.ws.close(); this.ws = null; }
		if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
	}

	updateLocation(location: LocationState): void {
		if (!isValidLocation(location)) return;
		if (this.state.participants[this.participantId]) {
			this.state.participants[this.participantId].location = location;
			this.state.participants[this.participantId].lastSeen = new Date().toISOString();
			this.send({ type: "location", participantId: this.participantId, location });
			this.notifyStateChange();
		}
	}

	clearLocation(): void {
		if (this.state.participants[this.participantId]) {
			delete this.state.participants[this.participantId].location;
			this.state.participants[this.participantId].lastSeen = new Date().toISOString();
			this.send({ type: "location", participantId: this.participantId, location: null as any });
			this.notifyStateChange();
		}
	}

	updateStatus(status: ParticipantStatus): void {
		if (this.state.participants[this.participantId]) {
			this.state.participants[this.participantId].status = status;
			this.state.participants[this.participantId].lastSeen = new Date().toISOString();
			this.send({ type: "status", participantId: this.participantId, status });
			this.notifyStateChange();
		}
	}

	addWaypoint(waypoint: WaypointState): void {
		this.state.waypoints.push(waypoint);
		this.send({ type: "waypoint_add", waypoint });
		this.notifyStateChange();
	}

	removeWaypoint(waypointId: string): void {
		this.state.waypoints = this.state.waypoints.filter((w) => w.id !== waypointId);
		this.send({ type: "waypoint_remove", waypointId });
		this.notifyStateChange();
	}

	sendRouteRequest(fromId: string, fromName: string, fromEmoji: string): void {
		this.send({ type: "route_request", fromId, fromName, fromEmoji });
	}

	getState(): RoomState {
		return { ...this.state };
	}
}
