/**
 * Cal module — temporal coordination calendar.
 *
 * Group calendars with lunar/solar/seasonal time systems,
 * location-aware events, and temporal-spatial zoom coupling.
 *
 * All persistence uses Automerge documents via SyncServer —
 * no PostgreSQL dependency.
 */

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 { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing";
import type { SyncServer } from '../../server/local-first/sync-server';
import { calendarSchema, calendarDocId } from './schemas';
import type { CalendarDoc, CalendarEvent, CalendarSource, ScheduledItemMetadata } from './schemas';

let _syncServer: SyncServer | null = null;

const routes = new Hono();

// ── Local-first helpers ──

/**
 * Lazily create the calendar Automerge doc if it doesn't exist yet.
 * Returns the current (immutable) doc snapshot.
 */
function ensureDoc(space: string): CalendarDoc {
	const docId = calendarDocId(space);
	let doc = _syncServer!.getDoc<CalendarDoc>(docId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<CalendarDoc>(), 'init calendar', (d) => {
			const init = calendarSchema.init();
			d.meta = init.meta;
			d.meta.spaceSlug = space;
			d.sources = {};
			d.events = {};
		});
		_syncServer!.setDoc(docId, doc);
	}
	return doc;
}

function daysFromNow(days: number, hours: number, minutes: number): Date {
	const d = new Date();
	d.setDate(d.getDate() + days);
	d.setHours(hours, minutes, 0, 0);
	return d;
}

/**
 * Build an event row object suitable for JSON responses.
 * Maps camelCase schema fields to the snake_case format the API previously returned.
 */
function eventToRow(ev: CalendarEvent, sources: Record<string, CalendarSource>) {
	const src = ev.sourceId ? sources[ev.sourceId] : undefined;
	return {
		id: ev.id,
		title: ev.title,
		description: ev.description,
		start_time: ev.startTime ? new Date(ev.startTime).toISOString() : null,
		end_time: ev.endTime ? new Date(ev.endTime).toISOString() : null,
		all_day: ev.allDay,
		timezone: ev.timezone,
		rrule: ev.rrule,
		status: ev.status,
		visibility: ev.visibility,
		source_id: ev.sourceId,
		source_name: src?.name ?? ev.sourceName ?? null,
		source_color: src?.color ?? ev.sourceColor ?? null,
		source_type: src?.sourceType ?? ev.sourceType ?? null,
		location_id: ev.locationId,
		location_name: ev.locationName,
		location_label: ev.locationName,
		location_lat: ev.locationLat,
		location_lng: ev.locationLng,
		location_granularity: ev.locationGranularity,
		is_virtual: ev.isVirtual,
		virtual_url: ev.virtualUrl,
		virtual_platform: ev.virtualPlatform,
		r_tool_source: ev.rToolSource,
		r_tool_entity_id: ev.rToolEntityId,
		attendees: ev.attendees,
		attendee_count: ev.attendeeCount,
		metadata: ev.metadata,
		created_at: ev.createdAt ? new Date(ev.createdAt).toISOString() : null,
		updated_at: ev.updatedAt ? new Date(ev.updatedAt).toISOString() : null,
	};
}

/**
 * Build a source row object for JSON responses.
 */
function sourceToRow(src: CalendarSource) {
	return {
		id: src.id,
		name: src.name,
		source_type: src.sourceType,
		url: src.url,
		color: src.color,
		is_active: src.isActive,
		is_visible: src.isVisible,
		sync_interval_minutes: src.syncIntervalMinutes,
		last_synced_at: src.lastSyncedAt ? new Date(src.lastSyncedAt).toISOString() : null,
		owner_id: src.ownerId,
		created_at: src.createdAt ? new Date(src.createdAt).toISOString() : null,
	};
}

/**
 * Seed demo data if the doc has no events yet.
 */
function seedDemoIfEmpty(space: string) {
	const docId = calendarDocId(space);
	const doc = ensureDoc(space);
	if (Object.keys(doc.events).length > 0) return;

	_syncServer!.changeDoc<CalendarDoc>(docId, 'seed demo data', (d) => {
		const now = Date.now();

		// Create calendar sources
		const communityId = crypto.randomUUID();
		const sprintsId = crypto.randomUUID();

		d.sources[communityId] = {
			id: communityId,
			name: 'Community Events',
			sourceType: 'MANUAL',
			url: null,
			color: '#6366f1',
			isActive: true,
			isVisible: true,
			syncIntervalMinutes: null,
			lastSyncedAt: 0,
			ownerId: null,
			createdAt: now,
		};
		d.sources[sprintsId] = {
			id: sprintsId,
			name: 'Development Sprints',
			sourceType: 'MANUAL',
			url: null,
			color: '#f59e0b',
			isActive: true,
			isVisible: true,
			syncIntervalMinutes: null,
			lastSyncedAt: 0,
			ownerId: null,
			createdAt: now,
		};

		// Location IDs (embedded on events, no separate locations table)
		const berlinLocId = crypto.randomUUID();

		// Seed events
		const seedEvents: Array<{
			title: string; desc: string; start: Date; end: Date;
			sourceId: string; allDay?: boolean;
			locationId?: string; locationName?: string;
			locationLat?: number; locationLng?: number; locationGranularity?: string;
			isVirtual?: boolean; virtualUrl?: string; virtualPlatform?: string;
		}> = [
			{
				title: "rSpace Launch Party",
				desc: "Celebrating the launch of the unified rSpace platform with all 22 modules live.",
				start: daysFromNow(-21, 18, 0), end: daysFromNow(-21, 22, 0),
				sourceId: communityId, locationName: "Radiant Hall, Pittsburgh",
			},
			{
				title: "Provider Onboarding Workshop",
				desc: "Hands-on session for print providers joining the cosmolocal network.",
				start: daysFromNow(-12, 14, 0), end: daysFromNow(-12, 17, 0),
				sourceId: communityId, isVirtual: true,
				virtualUrl: "https://meet.jit.si/rspace-providers", virtualPlatform: "Jitsi",
			},
			{
				title: "Weekly Community Standup",
				desc: "Open standup — share what you're working on, ask for help, coordinate.",
				start: daysFromNow(0, 16, 0), end: daysFromNow(0, 16, 45),
				sourceId: communityId, isVirtual: true,
				virtualUrl: "https://meet.jit.si/rspace-standup", virtualPlatform: "Jitsi",
			},
			{
				title: "Sprint: Module Seeding & Polish",
				desc: "Focus sprint on populating demo data and improving UX across all modules.",
				start: daysFromNow(0, 9, 0), end: daysFromNow(5, 18, 0),
				sourceId: sprintsId, allDay: true,
			},
			{
				title: "rFlows Budget Review",
				desc: "Quarterly review of treasury flows, enoughness thresholds, and overflow routing.",
				start: daysFromNow(6, 15, 0), end: daysFromNow(6, 17, 0),
				sourceId: communityId, isVirtual: true,
				virtualUrl: "https://meet.jit.si/rflows-review", virtualPlatform: "Jitsi",
			},
			{
				title: "Cosmolocal Design Sprint",
				desc: "Two-day design sprint on the next generation of cosmolocal tooling.",
				start: daysFromNow(11, 9, 0), end: daysFromNow(12, 18, 0),
				sourceId: sprintsId,
				locationId: berlinLocId, locationName: "Druckwerkstatt Berlin",
				locationLat: 52.52, locationLng: 13.405, locationGranularity: "city",
			},
			{
				title: "Q1 Retrospective",
				desc: "Looking back at what we built, what worked, and what to improve.",
				start: daysFromNow(21, 16, 0), end: daysFromNow(21, 18, 0),
				sourceId: communityId, isVirtual: true,
				virtualUrl: "https://meet.jit.si/rspace-retro", virtualPlatform: "Jitsi",
			},
		];

		for (const e of seedEvents) {
			const eventId = crypto.randomUUID();
			d.events[eventId] = {
				id: eventId,
				title: e.title,
				description: e.desc,
				startTime: e.start.getTime(),
				endTime: e.end.getTime(),
				allDay: e.allDay || false,
				timezone: 'UTC',
				rrule: null,
				status: null,
				visibility: null,
				sourceId: e.sourceId,
				sourceName: null,
				sourceType: null,
				sourceColor: null,
				locationId: e.locationId || null,
				locationName: e.locationName || null,
				coordinates: null,
				locationGranularity: e.locationGranularity || null,
				locationLat: e.locationLat ?? null,
				locationLng: e.locationLng ?? null,
				isVirtual: e.isVirtual || false,
				virtualUrl: e.virtualUrl || null,
				virtualPlatform: e.virtualPlatform || null,
				rToolSource: null,
				rToolEntityId: null,
				attendees: [],
				attendeeCount: 0,
				metadata: null,
				createdAt: now,
				updatedAt: now,
			};
		}
	});

	console.log("[Cal] Demo data seeded: 2 sources, 7 events");
}

// ── API: Events ──

// GET /api/events — query events with filters
routes.get("/api/events", async (c) => {
	const space = c.req.param("space") || "demo";
	const { start, end, source, search, rTool, rEntityId, upcoming } = c.req.query();

	const doc = ensureDoc(space);
	let events = Object.values(doc.events);

	// Apply filters
	if (start) {
		const startMs = new Date(start).getTime();
		events = events.filter((e) => e.startTime >= startMs);
	}
	if (end) {
		const endMs = new Date(end).getTime() + 86400000; // +1 day
		events = events.filter((e) => e.startTime <= endMs);
	}
	if (source) {
		events = events.filter((e) => e.sourceId === source);
	}
	if (search) {
		const term = search.toLowerCase();
		events = events.filter((e) =>
			e.title.toLowerCase().includes(term) ||
			(e.description && e.description.toLowerCase().includes(term))
		);
	}
	if (rTool) {
		events = events.filter((e) => e.rToolSource === rTool);
	}
	if (rEntityId) {
		events = events.filter((e) => e.rToolEntityId === rEntityId);
	}
	if (upcoming) {
		const nowMs = Date.now();
		const futureMs = nowMs + parseInt(upcoming) * 86400000;
		events = events.filter((e) => e.startTime >= nowMs && e.startTime <= futureMs);
	}

	// Sort by start time, limit to 500
	events.sort((a, b) => a.startTime - b.startTime);
	events = events.slice(0, 500);

	const rows = events.map((e) => eventToRow(e, doc.sources));
	return c.json({ count: rows.length, results: rows });
});

// POST /api/events — create event
routes.post("/api/events", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	let claims;
	try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const space = c.req.param("space") || "demo";
	const body = await c.req.json();
	const { title, description, start_time, end_time, all_day, timezone, source_id, location_id, location_name,
		is_virtual, virtual_url, virtual_platform, r_tool_source, r_tool_entity_id,
		is_scheduled_item, provenance, item_preview } = body;
	if (!title?.trim() || !start_time) return c.json({ error: "Title and start_time required" }, 400);

	const docId = calendarDocId(space);
	ensureDoc(space);
	const eventId = crypto.randomUUID();
	const now = Date.now();

	// Build metadata for scheduled knowledge items
	let metadata: ScheduledItemMetadata | null = null;
	if (is_scheduled_item && provenance) {
		metadata = {
			isScheduledItem: true,
			provenance: {
				rid: provenance.rid || '',
				contentHash: provenance.content_hash ?? provenance.contentHash ?? null,
				sourceType: provenance.source_type ?? provenance.sourceType ?? '',
				sourceSpace: provenance.source_space ?? provenance.sourceSpace ?? space,
				lifecycleEvent: provenance.lifecycle_event ?? provenance.lifecycleEvent ?? 'NEW',
				originalCreatedAt: provenance.original_created_at ?? provenance.originalCreatedAt ?? now,
				scheduledBy: provenance.scheduled_by ?? provenance.scheduledBy ?? null,
				scheduledAt: now,
			},
			itemPreview: item_preview ? {
				textPreview: item_preview.text_preview ?? item_preview.textPreview ?? '',
				mimeType: item_preview.mime_type ?? item_preview.mimeType ?? 'text/plain',
				thumbnailUrl: item_preview.thumbnail_url ?? item_preview.thumbnailUrl,
				canvasUrl: item_preview.canvas_url ?? item_preview.canvasUrl,
				shapeSnapshot: item_preview.shape_snapshot ?? item_preview.shapeSnapshot,
			} : {
				textPreview: description || title,
				mimeType: 'text/plain',
			},
			reminderSent: false,
			reminderSentAt: null,
		};
	}

	_syncServer!.changeDoc<CalendarDoc>(docId, `create event ${eventId}`, (d) => {
		d.events[eventId] = {
			id: eventId,
			title: title.trim(),
			description: description || '',
			startTime: new Date(start_time).getTime(),
			endTime: end_time ? new Date(end_time).getTime() : 0,
			allDay: all_day || false,
			timezone: timezone || 'UTC',
			rrule: null,
			status: null,
			visibility: null,
			sourceId: source_id || null,
			sourceName: null,
			sourceType: null,
			sourceColor: null,
			locationId: location_id || null,
			locationName: location_name || null,
			coordinates: null,
			locationGranularity: null,
			locationLat: null,
			locationLng: null,
			isVirtual: is_virtual || false,
			virtualUrl: virtual_url || null,
			virtualPlatform: virtual_platform || null,
			rToolSource: r_tool_source || 'rSchedule',
			rToolEntityId: r_tool_entity_id || null,
			attendees: [],
			attendeeCount: 0,
			metadata: metadata,
			createdAt: now,
			updatedAt: now,
		};
	});

	const updated = _syncServer!.getDoc<CalendarDoc>(docId)!;
	return c.json(eventToRow(updated.events[eventId], updated.sources), 201);
});

// GET /api/events/scheduled — query only scheduled knowledge items
routes.get("/api/events/scheduled", async (c) => {
	const space = c.req.param("space") || "demo";
	const { date, upcoming, pending_only } = c.req.query();

	const doc = ensureDoc(space);
	let events = Object.values(doc.events).filter((e) => {
		const meta = e.metadata as ScheduledItemMetadata | null;
		return meta?.isScheduledItem === true;
	});

	if (date) {
		const dayStart = new Date(date).setHours(0, 0, 0, 0);
		const dayEnd = dayStart + 86400000;
		events = events.filter((e) => e.startTime >= dayStart && e.startTime < dayEnd);
	}

	if (upcoming) {
		const nowMs = Date.now();
		const futureMs = nowMs + parseInt(upcoming) * 86400000;
		events = events.filter((e) => e.startTime >= nowMs && e.startTime <= futureMs);
	}

	if (pending_only === "true") {
		events = events.filter((e) => {
			const meta = e.metadata as ScheduledItemMetadata;
			return !meta.reminderSent;
		});
	}

	events.sort((a, b) => a.startTime - b.startTime);
	const rows = events.map((e) => eventToRow(e, doc.sources));
	return c.json({ count: rows.length, results: rows });
});

// GET /api/events/:id
routes.get("/api/events/:id", async (c) => {
	const space = c.req.param("space") || "demo";
	const id = c.req.param("id");
	const doc = ensureDoc(space);

	const ev = doc.events[id];
	if (!ev) return c.json({ error: "Event not found" }, 404);
	return c.json(eventToRow(ev, doc.sources));
});

// PATCH /api/events/:id
routes.patch("/api/events/:id", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	let claims;
	try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const space = c.req.param("space") || "demo";
	const id = c.req.param("id");
	const body = await c.req.json();

	const docId = calendarDocId(space);
	const doc = ensureDoc(space);
	if (!doc.events[id]) return c.json({ error: "Not found" }, 404);

	// Map of allowed body keys to CalendarEvent fields
	const fieldMap: Record<string, keyof CalendarEvent> = {
		title: 'title',
		description: 'description',
		start_time: 'startTime',
		end_time: 'endTime',
		all_day: 'allDay',
		timezone: 'timezone',
		status: 'status',
		visibility: 'visibility',
		location_name: 'locationName',
		is_virtual: 'isVirtual',
		virtual_url: 'virtualUrl',
		metadata: 'metadata',
	};

	const updates: Array<{ field: keyof CalendarEvent; value: any }> = [];
	for (const [bodyKey, docField] of Object.entries(fieldMap)) {
		if (body[bodyKey] !== undefined) {
			let value = body[bodyKey];
			// Convert time strings to epoch ms
			if (bodyKey === 'start_time' || bodyKey === 'end_time') {
				value = new Date(value).getTime();
			}
			updates.push({ field: docField, value });
		}
	}

	if (updates.length === 0) return c.json({ error: "No fields" }, 400);

	_syncServer!.changeDoc<CalendarDoc>(docId, `update event ${id}`, (d) => {
		const ev = d.events[id];
		for (const { field, value } of updates) {
			(ev as any)[field] = value;
		}
		ev.updatedAt = Date.now();
	});

	const updated = _syncServer!.getDoc<CalendarDoc>(docId)!;
	return c.json(eventToRow(updated.events[id], updated.sources));
});

// DELETE /api/events/:id
routes.delete("/api/events/:id", async (c) => {
	const space = c.req.param("space") || "demo";
	const id = c.req.param("id");

	const docId = calendarDocId(space);
	const doc = ensureDoc(space);
	if (!doc.events[id]) return c.json({ error: "Not found" }, 404);

	_syncServer!.changeDoc<CalendarDoc>(docId, `delete event ${id}`, (d) => {
		delete d.events[id];
	});
	return c.json({ ok: true });
});

// ── API: Sources ──

routes.get("/api/sources", async (c) => {
	const space = c.req.param("space") || "demo";
	const { is_active, is_visible, source_type } = c.req.query();
	const doc = ensureDoc(space);

	let sources = Object.values(doc.sources);

	if (is_active !== undefined) {
		const active = is_active === "true";
		sources = sources.filter((s) => s.isActive === active);
	}
	if (is_visible !== undefined) {
		const visible = is_visible === "true";
		sources = sources.filter((s) => s.isVisible === visible);
	}
	if (source_type) {
		sources = sources.filter((s) => s.sourceType === source_type);
	}

	sources.sort((a, b) => a.name.localeCompare(b.name));
	const rows = sources.map(sourceToRow);
	return c.json({ count: rows.length, results: rows });
});

routes.post("/api/sources", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	try { await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const space = c.req.param("space") || "demo";
	const body = await c.req.json();
	const docId = calendarDocId(space);
	ensureDoc(space);

	const sourceId = crypto.randomUUID();
	const now = Date.now();

	_syncServer!.changeDoc<CalendarDoc>(docId, `create source ${sourceId}`, (d) => {
		d.sources[sourceId] = {
			id: sourceId,
			name: body.name,
			sourceType: body.source_type || 'MANUAL',
			url: body.url || null,
			color: body.color || '#6366f1',
			isActive: body.is_active ?? true,
			isVisible: body.is_visible ?? true,
			syncIntervalMinutes: null,
			lastSyncedAt: 0,
			ownerId: null,
			createdAt: now,
		};
	});

	const updated = _syncServer!.getDoc<CalendarDoc>(docId)!;
	return c.json(sourceToRow(updated.sources[sourceId]), 201);
});

// ── API: Locations ──
// Locations are now derived from event data (no separate table).
// Each unique locationId/locationName combination is extracted from events.

interface DerivedLocation {
	id: string;
	name: string;
	granularity: number | null;
	parent_id: string | null;
	lat: number | null;
	lng: number | null;
}

function deriveLocations(doc: CalendarDoc): DerivedLocation[] {
	const seen = new Map<string, DerivedLocation>();
	for (const ev of Object.values(doc.events)) {
		const key = ev.locationId || ev.locationName;
		if (!key) continue;
		if (seen.has(key)) continue;
		seen.set(key, {
			id: ev.locationId || key,
			name: ev.locationName || key,
			granularity: ev.locationGranularity ? parseInt(ev.locationGranularity) || null : null,
			parent_id: null,
			lat: ev.locationLat,
			lng: ev.locationLng,
		});
	}
	return Array.from(seen.values());
}

routes.get("/api/locations", async (c) => {
	const space = c.req.param("space") || "demo";
	const { granularity, parent, search, root } = c.req.query();
	const doc = ensureDoc(space);

	let locations = deriveLocations(doc);

	if (root === "true") {
		locations = locations.filter((l) => l.parent_id === null);
	}
	if (granularity) {
		const g = parseInt(granularity);
		locations = locations.filter((l) => l.granularity === g);
	}
	if (parent) {
		locations = locations.filter((l) => l.parent_id === parent);
	}
	if (search) {
		const term = search.toLowerCase();
		locations = locations.filter((l) => l.name.toLowerCase().includes(term));
	}

	locations.sort((a, b) => a.name.localeCompare(b.name));
	return c.json(locations);
});

routes.get("/api/locations/tree", async (c) => {
	const space = c.req.param("space") || "demo";
	const doc = ensureDoc(space);

	// Flat list with depth=0 since hierarchical parent_id data is not stored in Automerge
	const locations = deriveLocations(doc).map((l) => ({ ...l, depth: 0 }));
	locations.sort((a, b) => a.name.localeCompare(b.name));
	return c.json(locations);
});

// ── API: Lunar data (computed, not stored) ──

routes.get("/api/lunar", async (c) => {
	const { start, end } = c.req.query();
	if (!start || !end) return c.json({ error: "start and end required" }, 400);

	// Simple lunar phase approximation based on synodic month
	const SYNODIC_MONTH = 29.53059;
	const KNOWN_NEW_MOON = new Date("2024-01-11T11:57:00Z").getTime();
	const phases: Record<string, { phase: string; illumination: number }> = {};

	const startDate = new Date(start);
	const endDate = new Date(end);
	const current = new Date(startDate);

	while (current <= endDate) {
		const daysSinceNewMoon = (current.getTime() - KNOWN_NEW_MOON) / (1000 * 60 * 60 * 24);
		const lunation = ((daysSinceNewMoon % SYNODIC_MONTH) + SYNODIC_MONTH) % SYNODIC_MONTH;
		const fraction = lunation / SYNODIC_MONTH;
		const illumination = 0.5 * (1 - Math.cos(2 * Math.PI * fraction));

		let phase = "waxing_crescent";
		if (fraction < 0.0625) phase = "new_moon";
		else if (fraction < 0.1875) phase = "waxing_crescent";
		else if (fraction < 0.3125) phase = "first_quarter";
		else if (fraction < 0.4375) phase = "waxing_gibbous";
		else if (fraction < 0.5625) phase = "full_moon";
		else if (fraction < 0.6875) phase = "waning_gibbous";
		else if (fraction < 0.8125) phase = "last_quarter";
		else if (fraction < 0.9375) phase = "waning_crescent";
		else phase = "new_moon";

		phases[current.toISOString().split("T")[0]] = { phase, illumination: Math.round(illumination * 100) / 100 };
		current.setDate(current.getDate() + 1);
	}

	return c.json(phases);
});

// ── API: Stats ──

routes.get("/api/stats", async (c) => {
	const space = c.req.param("space") || "demo";
	const doc = ensureDoc(space);

	const events = Object.values(doc.events).length;
	const sources = Object.values(doc.sources).filter((s) => s.isActive).length;
	const locations = deriveLocations(doc).length;

	return c.json({ events, sources, locations });
});

// ── API: Context (r* tool bridge) ──

routes.get("/api/context/:tool", async (c) => {
	const space = c.req.param("space") || "demo";
	const tool = c.req.param("tool");
	const entityId = c.req.query("entityId");
	if (!entityId) return c.json({ error: "entityId required" }, 400);

	const doc = ensureDoc(space);
	const matching = Object.values(doc.events)
		.filter((e) => e.rToolSource === tool && e.rToolEntityId === entityId)
		.sort((a, b) => a.startTime - b.startTime);

	const rows = matching.map((e) => eventToRow(e, doc.sources));
	return c.json({ count: rows.length, results: rows });
});

// ── Page route ──
routes.get("/", (c) => {
	const space = c.req.param("space") || "demo";
	return c.html(renderShell({
		title: `${space} — Calendar | rSpace`,
		moduleId: "rcal",
		spaceSlug: space,
		modules: getModuleInfoList(),
		theme: "dark",
		body: `<folk-calendar-view space="${space}"></folk-calendar-view>`,
		scripts: `<script type="module" src="/modules/rcal/folk-calendar-view.js?v=4"></script>`,
		styles: `<link rel="stylesheet" href="/modules/rcal/cal.css?v=2">
			<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin="">`,
	}));
});

export const calModule: RSpaceModule = {
	id: "rcal",
	name: "rCal",
	icon: "📅",
	description: "Temporal coordination calendar with lunar, solar, and seasonal systems",
	scoping: { defaultScope: 'global', userConfigurable: true },
	docSchemas: [{ pattern: '{space}:cal:events', description: 'Calendar events and sources', init: calendarSchema.init }],
	routes,
	standaloneDomain: "rcal.online",
	landingPage: renderLanding,
	seedTemplate: seedDemoIfEmpty,
	async onInit(ctx) {
		_syncServer = ctx.syncServer;
		// Seed demo data for the default space
		seedDemoIfEmpty("demo");
	},
	feeds: [
		{
			id: "events",
			name: "Events",
			kind: "data",
			description: "Calendar events with times, locations, and virtual meeting links",
			filterable: true,
		},
		{
			id: "availability",
			name: "Availability",
			kind: "data",
			description: "Free/busy windows and scheduling availability across calendars",
		},
	],
	acceptsFeeds: ["data", "governance"],
	outputPaths: [
		{ path: "saved-views", name: "Saved Views", icon: "👁️", description: "Custom calendar views and filters" },
		{ path: "events", name: "Events", icon: "📅", description: "Calendar events across all systems" },
	],
};
