/**
 * Maps module — real-time collaborative location sharing.
 *
 * Port of rmaps-online. Rooms are ephemeral (no DB). Proxies to
 * the sync server for WebSocket-based location sharing. Embeds
 * MapLibre GL for outdoor maps and c3nav for indoor maps.
 */

import { Hono } from "hono";
import * as Automerge from "@automerge/automerge";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { verifyToken, extractToken } from "../../server/auth";
import { renderLanding } from "./landing";
import type { SyncServer } from '../../server/local-first/sync-server';
import { mapsSchema, mapsDocId } from './schemas';
import type { MapsDoc, MapAnnotation, SavedRoute, SavedMeetingPoint } from './schemas';

let _syncServer: SyncServer | null = null;

const routes = new Hono();

const SYNC_SERVER = process.env.MAPS_SYNC_URL || "http://localhost:3001";

// ── Local-first helpers ──

function ensureMapsDoc(space: string): MapsDoc {
	const docId = mapsDocId(space);
	let doc = _syncServer!.getDoc<MapsDoc>(docId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<MapsDoc>(), 'init maps', (d) => {
			const init = mapsSchema.init();
			Object.assign(d, init);
			d.meta.spaceSlug = space;
		});
		_syncServer!.setDoc(docId, doc);
	}
	return doc;
}

// ── CRUD: Pins (annotations) ──

routes.get("/api/pins", (c) => {
	if (!_syncServer) return c.json({ pins: [] });
	const space = c.req.param("space") || "demo";
	const doc = ensureMapsDoc(space);
	return c.json({ pins: Object.values(doc.annotations || {}) });
});

routes.post("/api/pins", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	let claims;
	try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
	if (!_syncServer) return c.json({ error: "Not initialized" }, 503);
	const space = c.req.param("space") || "demo";
	const { type = 'pin', lat, lng, label = '' } = await c.req.json();
	if (lat == null || lng == null) return c.json({ error: "lat and lng required" }, 400);
	const id = crypto.randomUUID();
	const docId = mapsDocId(space);
	ensureMapsDoc(space);
	_syncServer.changeDoc<MapsDoc>(docId, `add pin ${id}`, (d) => {
		d.annotations[id] = { id, type: type as 'pin' | 'note' | 'area', lat, lng, label, authorDid: (claims.did as string) || claims.sub || null, createdAt: Date.now() };
	});
	const updated = _syncServer.getDoc<MapsDoc>(docId)!;
	return c.json(updated.annotations[id], 201);
});

routes.delete("/api/pins/:pinId", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
	if (!_syncServer) return c.json({ error: "Not initialized" }, 503);
	const space = c.req.param("space") || "demo";
	const pinId = c.req.param("pinId");
	const docId = mapsDocId(space);
	const doc = ensureMapsDoc(space);
	if (!doc.annotations[pinId]) return c.json({ error: "Not found" }, 404);
	_syncServer.changeDoc<MapsDoc>(docId, `delete pin ${pinId}`, (d) => { delete d.annotations[pinId]; });
	return c.json({ ok: true });
});

// ── CRUD: Saved Routes ──

routes.get("/api/saved-routes", (c) => {
	if (!_syncServer) return c.json({ routes: [] });
	const space = c.req.param("space") || "demo";
	const doc = ensureMapsDoc(space);
	return c.json({ routes: Object.values(doc.savedRoutes || {}) });
});

routes.post("/api/saved-routes", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	let claims;
	try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
	if (!_syncServer) return c.json({ error: "Not initialized" }, 503);
	const space = c.req.param("space") || "demo";
	const { name, waypoints } = await c.req.json();
	if (!name || !Array.isArray(waypoints) || waypoints.length < 2) return c.json({ error: "name and waypoints (min 2) required" }, 400);
	const id = crypto.randomUUID();
	const docId = mapsDocId(space);
	ensureMapsDoc(space);
	_syncServer.changeDoc<MapsDoc>(docId, `save route ${id}`, (d) => {
		d.savedRoutes[id] = { id, name, waypoints, authorDid: (claims.did as string) || claims.sub || null, createdAt: Date.now() };
	});
	const updated = _syncServer.getDoc<MapsDoc>(docId)!;
	return c.json(updated.savedRoutes[id], 201);
});

routes.delete("/api/saved-routes/:routeId", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
	if (!_syncServer) return c.json({ error: "Not initialized" }, 503);
	const space = c.req.param("space") || "demo";
	const routeId = c.req.param("routeId");
	const docId = mapsDocId(space);
	const doc = ensureMapsDoc(space);
	if (!doc.savedRoutes[routeId]) return c.json({ error: "Not found" }, 404);
	_syncServer.changeDoc<MapsDoc>(docId, `delete route ${routeId}`, (d) => { delete d.savedRoutes[routeId]; });
	return c.json({ ok: true });
});

// ── CRUD: Meeting Points ──

routes.get("/api/meeting-points", (c) => {
	if (!_syncServer) return c.json({ meetingPoints: [] });
	const space = c.req.param("space") || "demo";
	const doc = ensureMapsDoc(space);
	return c.json({ meetingPoints: Object.values(doc.savedMeetingPoints || {}) });
});

routes.post("/api/meeting-points", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	let claims;
	try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
	if (!_syncServer) return c.json({ error: "Not initialized" }, 503);
	const space = c.req.param("space") || "demo";
	const { name, lat, lng } = await c.req.json();
	if (!name || lat == null || lng == null) return c.json({ error: "name, lat, and lng required" }, 400);
	const id = crypto.randomUUID();
	const docId = mapsDocId(space);
	ensureMapsDoc(space);
	_syncServer.changeDoc<MapsDoc>(docId, `save meeting point ${id}`, (d) => {
		d.savedMeetingPoints[id] = { id, name, lat, lng, setBy: (claims.did as string) || claims.sub || null, createdAt: Date.now() };
	});
	const updated = _syncServer.getDoc<MapsDoc>(docId)!;
	return c.json(updated.savedMeetingPoints[id], 201);
});

routes.delete("/api/meeting-points/:pointId", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }
	if (!_syncServer) return c.json({ error: "Not initialized" }, 503);
	const space = c.req.param("space") || "demo";
	const pointId = c.req.param("pointId");
	const docId = mapsDocId(space);
	const doc = ensureMapsDoc(space);
	if (!doc.savedMeetingPoints[pointId]) return c.json({ error: "Not found" }, 404);
	_syncServer.changeDoc<MapsDoc>(docId, `delete meeting point ${pointId}`, (d) => { delete d.savedMeetingPoints[pointId]; });
	return c.json({ ok: true });
});

// ── Sync URL for client-side WebSocket connection ──
routes.get("/api/sync-url", (c) => {
	const wsUrl = process.env.MAPS_SYNC_URL || "wss://maps-sync.rspace.online";
	return c.json({ syncUrl: wsUrl });
});

// ── Proxy: sync server health ──
routes.get("/api/health", async (c) => {
	try {
		const res = await fetch(`${SYNC_SERVER}/health`, { signal: AbortSignal.timeout(3000) });
		if (res.ok) return c.json(await res.json());
		return c.json({ status: "degraded", sync: false });
	} catch {
		return c.json({ status: "degraded", sync: false });
	}
});

// ── Proxy: room stats ──
routes.get("/api/stats", async (c) => {
	try {
		const res = await fetch(`${SYNC_SERVER}/stats`, { signal: AbortSignal.timeout(3000) });
		if (res.ok) return c.json(await res.json());
	} catch {}
	return c.json({ rooms: {} });
});

// ── Proxy: push notification VAPID key ──
routes.get("/api/push/vapid-public-key", async (c) => {
	try {
		const res = await fetch(`${SYNC_SERVER}/push/vapid-public-key`, { signal: AbortSignal.timeout(3000) });
		if (res.ok) return c.json(await res.json());
	} catch {}
	return c.json({ error: "Push not available" }, 503);
});

// ── Proxy: push subscribe/unsubscribe ──
routes.post("/api/push/subscribe", async (c) => {
	const body = await c.req.json();
	const res = await fetch(`${SYNC_SERVER}/push/subscribe`, {
		method: "POST",
		headers: { "Content-Type": "application/json" },
		body: JSON.stringify(body),
	});
	return c.json(await res.json(), res.status as any);
});

routes.post("/api/push/unsubscribe", async (c) => {
	const body = await c.req.json();
	const res = await fetch(`${SYNC_SERVER}/push/unsubscribe`, {
		method: "POST",
		headers: { "Content-Type": "application/json" },
		body: JSON.stringify(body),
	});
	return c.json(await res.json(), res.status as any);
});

// ── Proxy: request location (ping) ──
routes.post("/api/push/request-location", async (c) => {
	const body = await c.req.json();
	const res = await fetch(`${SYNC_SERVER}/push/request-location`, {
		method: "POST",
		headers: { "Content-Type": "application/json" },
		body: JSON.stringify(body),
	});
	return c.json(await res.json(), res.status as any);
});

// ── Proxy: last-known-location (offline ping response) ──
routes.post("/api/push/last-known-location", async (c) => {
	const body = await c.req.json();
	try {
		const res = await fetch(`${SYNC_SERVER}/push/last-known-location`, {
			method: "POST",
			headers: { "Content-Type": "application/json" },
			body: JSON.stringify(body),
		});
		if (res.ok) return c.json(await res.json());
	} catch {}
	return c.json({ received: true });
});

// ── Proxy: routing (OSRM + c3nav) ──
routes.post("/api/routing", async (c) => {
	const body = await c.req.json();
	const { from, to, mode = "walking", accessibility, indoor } = body;

	if (!from?.lat || !from?.lng || !to?.lat || !to?.lng) {
		return c.json({ error: "from and to with lat/lng required" }, 400);
	}

	const segments: { type: string; coordinates: [number, number][]; distance: number; duration: number; steps?: any[] }[] = [];
	let totalDistance = 0;
	let estimatedTime = 0;

	// ── Indoor routing via c3nav ──
	if (indoor?.event && (from.indoor || to.indoor)) {
		try {
			// If both are indoor, route fully indoors
			if (from.indoor && to.indoor) {
				const c3navRes = await fetch(`https://${indoor.event}.c3nav.de/api/v2/routing/route/`, {
					method: "POST",
					headers: { "Content-Type": "application/json", "X-API-Key": "anonymous", Accept: "application/json", "User-Agent": "rMaps/1.0" },
					body: JSON.stringify({
						origin: { level: from.indoor.level, x: from.indoor.x, y: from.indoor.y },
						destination: { level: to.indoor.level, x: to.indoor.x, y: to.indoor.y },
						...(accessibility?.avoidStairs ? { options: { avoid_stairs: true } } : {}),
						...(accessibility?.wheelchair ? { options: { wheelchair: true } } : {}),
					}),
					signal: AbortSignal.timeout(8000),
				});
				if (c3navRes.ok) {
					const data = await c3navRes.json();
					const coords: [number, number][] = data.path?.map((p: any) => [p.lng || p.x, p.lat || p.y]) || [];
					const dist = data.distance || 0;
					const dur = data.duration || Math.round(dist / 1.2);
					segments.push({ type: "indoor", coordinates: coords, distance: dist, duration: dur });
					totalDistance += dist;
					estimatedTime += dur;
				}
			} else {
				// Mixed indoor/outdoor: transition segment + outdoor OSRM
				const outdoorPoint = from.indoor ? to : from;
				const indoorPoint = from.indoor ? from : to;
				const transitionCoords: [number, number][] = [
					[indoorPoint.lng, indoorPoint.lat],
					[outdoorPoint.lng, outdoorPoint.lat],
				];
				segments.push({ type: "transition", coordinates: transitionCoords, distance: 50, duration: 30 });
				totalDistance += 50;
				estimatedTime += 30;

				// OSRM for the outdoor portion
				const profile = mode === "driving" ? "car" : "foot";
				const osrmRes = await fetch(
					`https://router.project-osrm.org/route/v1/${profile}/${outdoorPoint.lng},${outdoorPoint.lat};${(from.indoor ? to : from).lng},${(from.indoor ? to : from).lat}?overview=full&geometries=geojson&steps=true`,
					{ signal: AbortSignal.timeout(10000) },
				);
				if (osrmRes.ok) {
					const osrmData = await osrmRes.json();
					const route = osrmData.routes?.[0];
					if (route) {
						segments.push({
							type: "outdoor",
							coordinates: route.geometry?.coordinates || [],
							distance: route.distance || 0,
							duration: route.duration || 0,
							steps: route.legs?.[0]?.steps,
						});
						totalDistance += route.distance || 0;
						estimatedTime += route.duration || 0;
					}
				}
			}

			if (segments.length > 0) {
				return c.json({
					success: true,
					route: { segments, totalDistance, estimatedTime: Math.round(estimatedTime) },
				});
			}
		} catch { /* fall through to outdoor-only routing */ }
	}

	// ── Outdoor-only routing via OSRM ──
	const profile = mode === "driving" ? "car" : "foot";
	try {
		const res = await fetch(
			`https://router.project-osrm.org/route/v1/${profile}/${from.lng},${from.lat};${to.lng},${to.lat}?overview=full&geometries=geojson&steps=true`,
			{ signal: AbortSignal.timeout(10000) },
		);
		if (res.ok) {
			const data = await res.json();
			const route = data.routes?.[0];
			if (route) {
				return c.json({
					success: true,
					route: {
						segments: [{
							type: "outdoor",
							coordinates: route.geometry?.coordinates || [],
							distance: route.distance || 0,
							duration: route.duration || 0,
							steps: route.legs?.[0]?.steps,
						}],
						totalDistance: route.distance || 0,
						estimatedTime: Math.round(route.duration || 0),
					},
				});
			}
		}
	} catch {}
	return c.json({ error: "Routing failed" }, 502);
});

// ── Proxy: c3nav API ──
const VALID_C3NAV_EVENTS = ["39c3", "38c3", "37c3", "eh22", "eh2025", "camp2023"];
const ALLOWED_C3NAV_ENDPOINTS = ["map/settings", "map/bounds", "map/locations", "map/locations/full", "map/projection"];

// ── c3nav tile proxy ──
routes.get("/api/c3nav/tiles/:event/:level/:z/:x/:y", async (c) => {
	const event = c.req.param("event");
	const level = parseInt(c.req.param("level"), 10);
	const z = c.req.param("z");
	const x = c.req.param("x");
	const y = c.req.param("y");

	if (!VALID_C3NAV_EVENTS.includes(event)) return c.json({ error: "Invalid event" }, 400);
	if (isNaN(level) || level < -1 || level > 10) return c.json({ error: "Invalid level" }, 400);

	try {
		const res = await fetch(`https://tiles.${event}.c3nav.de/${level}/${z}/${x}/${y}.png`, {
			headers: { "User-Agent": "rMaps/1.0" },
			signal: AbortSignal.timeout(8000),
		});
		if (!res.ok) return c.json({ error: "Tile fetch failed" }, 502);

		const data = await res.arrayBuffer();
		return new Response(data, {
			headers: {
				"Content-Type": "image/png",
				"Cache-Control": "public, max-age=3600",
			},
		});
	} catch {
		return c.json({ error: "c3nav tile proxy error" }, 502);
	}
});

routes.get("/api/c3nav/:event", async (c) => {
	const event = c.req.param("event");
	const endpoint = c.req.query("endpoint") || "map/bounds";

	if (!VALID_C3NAV_EVENTS.includes(event)) return c.json({ error: "Invalid event" }, 400);
	const isAllowed = ALLOWED_C3NAV_ENDPOINTS.some((a) => endpoint === a || endpoint.startsWith(a + "/"));
	if (!isAllowed && !endpoint.startsWith("map/locations/")) return c.json({ error: "Endpoint not allowed" }, 403);

	try {
		const res = await fetch(`https://${event}.c3nav.de/api/v2/${endpoint}/`, {
			headers: { "X-API-Key": "anonymous", Accept: "application/json", "User-Agent": "rMaps/1.0" },
			signal: AbortSignal.timeout(5000),
		});
		if (res.ok) return c.json(await res.json());
		return c.json({ error: "c3nav API error" }, res.status as any);
	} catch {
		return c.json({ error: "c3nav unreachable" }, 502);
	}
});

// ── Page route ──
routes.get("/", (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = c.get("effectiveSpace") || space;
	return c.html(renderShell({
		title: `${space} — Maps | rSpace`,
		moduleId: "rmaps",
		spaceSlug: space,
		modules: getModuleInfoList(),
		body: `<folk-map-viewer space="${space}"></folk-map-viewer>`,
		scripts: `<link rel="preload" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4.1.2/dist/maplibre-gl.js" as="script">
			<link rel="preload" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4.1.2/dist/maplibre-gl.css" as="style">
			<script type="module" src="/modules/rmaps/folk-map-viewer.js?v=7"></script>`,
		styles: `<link rel="stylesheet" href="/modules/rmaps/maps.css?v=3">`,
	}));
});

// Room-specific page
routes.get("/:room", (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = c.get("effectiveSpace") || space;
	const room = c.req.param("room");
	return c.html(renderShell({
		title: `${room} — Maps | rSpace`,
		moduleId: "rmaps",
		spaceSlug: space,
		modules: getModuleInfoList(),
		styles: `<link rel="stylesheet" href="/modules/rmaps/maps.css?v=3">`,
		body: `<folk-map-viewer space="${space}" room="${room}"></folk-map-viewer>`,
		scripts: `<link rel="preload" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4.1.2/dist/maplibre-gl.js" as="script">
			<link rel="preload" href="https://cdn.jsdelivr.net/npm/maplibre-gl@4.1.2/dist/maplibre-gl.css" as="style">
			<script type="module" src="/modules/rmaps/folk-map-viewer.js?v=7"></script>`,
	}));
});

// ── MI Integration ──

export function getMapPinsForMI(space: string, limit = 10): { id: string; type: string; lat: number; lng: number; label: string; createdAt: number }[] {
	if (!_syncServer) return [];
	const docId = mapsDocId(space);
	const doc = _syncServer.getDoc<MapsDoc>(docId);
	if (!doc) return [];
	return Object.values(doc.annotations)
		.sort((a, b) => b.createdAt - a.createdAt)
		.slice(0, limit)
		.map((a) => ({ id: a.id, type: a.type, lat: a.lat, lng: a.lng, label: a.label, createdAt: a.createdAt }));
}

export const mapsModule: RSpaceModule = {
	id: "rmaps",
	name: "rMaps",
	icon: "🗺",
	description: "Real-time collaborative location sharing and indoor/outdoor maps",
	canvasShapes: ["folk-map"],
	canvasToolIds: ["create_map"],
	scoping: { defaultScope: 'global', userConfigurable: false },
	docSchemas: [{ pattern: '{space}:maps:annotations', description: 'Map annotations, routes, and meeting points', init: mapsSchema.init }],
	routes,
	landingPage: renderLanding,
	standaloneDomain: "rmaps.online",
	async onInit(ctx) { _syncServer = ctx.syncServer; },
	feeds: [
		{
			id: "locations",
			name: "Locations",
			kind: "data",
			description: "Shared location pins, points of interest, and room positions",
			filterable: true,
		},
		{
			id: "routes",
			name: "Routes",
			kind: "data",
			description: "Calculated routes and navigation paths between locations",
		},
	],
	acceptsFeeds: ["data"],
	outputPaths: [
		{ path: "routes", name: "Routes", icon: "🛤️", description: "Saved navigation routes" },
		{ path: "places", name: "Places", icon: "📍", description: "Saved locations and points of interest" },
	],
};
