/**
 * rChats module — encrypted community messaging with channels, threads, DMs.
 *
 * Local-first via Automerge CRDT. One doc per channel, directory doc per space.
 * DMs as private two-member channels with deterministic doc IDs.
 */

import { Hono } from "hono";
import * as Automerge from "@automerge/automerge";
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule, SpaceLifecycleContext } from "../../shared/module";
import { verifyToken, extractToken } from "../../server/auth";
import { resolveCallerRole } from "../../server/spaces";
import type { SpaceRoleString } from "../../server/spaces";
import { filterByVisibility, filterArrayByVisibility } from "../../shared/membrane";
import { notify } from '../../server/notification-service';
import { renderLanding } from "./landing";
import type { SyncServer } from '../../server/local-first/sync-server';
import {
	chatsDirectorySchema, chatChannelSchema,
	chatsDirectoryDocId, chatChannelDocId, dmChannelDocId,
} from './schemas';
import type {
	ChatsDirectoryDoc, ChatChannelDoc, ChannelInfo, ChatMessage,
	Transclusion, ThreadMeta,
} from './schemas';

let _syncServer: SyncServer | null = null;

const routes = new Hono();

// ── Local-first helpers ──

function ensureDirectoryDoc(space: string): ChatsDirectoryDoc {
	const docId = chatsDirectoryDocId(space);
	let doc = _syncServer!.getDoc<ChatsDirectoryDoc>(docId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<ChatsDirectoryDoc>(), 'init chats directory', (d) => {
			const init = chatsDirectorySchema.init();
			Object.assign(d, init);
			d.meta.spaceSlug = space;
		});
		_syncServer!.setDoc(docId, doc);
	}
	return doc;
}

function ensureChannelDoc(space: string, channelId: string, isDm = false): ChatChannelDoc {
	const docId = isDm ? channelId : chatChannelDocId(space, channelId);
	let doc = _syncServer!.getDoc<ChatChannelDoc>(docId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<ChatChannelDoc>(), 'init channel', (d) => {
			const init = chatChannelSchema.init();
			Object.assign(d, init);
			d.meta.spaceSlug = space;
			d.channelId = channelId;
			d.isDm = isDm;
		});
		_syncServer!.setDoc(docId, doc);
	}
	return doc;
}

/** Seed "general" channel if none exist */
function seedGeneralChannel(space: string) {
	if (!_syncServer) return;
	const dir = ensureDirectoryDoc(space);
	if (Object.keys(dir.channels || {}).length > 0) return;

	const id = 'general';
	const dirDocId = chatsDirectoryDocId(space);
	_syncServer.changeDoc<ChatsDirectoryDoc>(dirDocId, 'seed general channel', (d) => {
		d.channels[id] = {
			id, name: 'general', description: 'General discussion',
			isPrivate: false, isDm: false, createdBy: null,
			createdAt: Date.now(), updatedAt: Date.now(),
		};
	});
	ensureChannelDoc(space, id);
}

/** Extract @mentions from message content */
function extractMentions(content: string): string[] {
	const matches = content.match(/@([a-zA-Z0-9_.-]+)/g);
	return matches ? matches.map(m => m.slice(1)) : [];
}

// ── Auth helper ──

async function requireAuth(c: any): Promise<{ claims: any; role: SpaceRoleString; space: string } | null> {
	const token = extractToken(c.req.raw.headers);
	if (!token) { c.json({ error: "Authentication required" }, 401); return null; }
	let claims;
	try { claims = await verifyToken(token); } catch { c.json({ error: "Invalid token" }, 401); return null; }
	if (!_syncServer) { c.json({ error: "Not initialized" }, 503); return null; }
	const space = c.req.param("space") || "demo";
	let role: SpaceRoleString = 'viewer';
	const resolved = await resolveCallerRole(space, claims);
	if (resolved) role = resolved.role;
	return { claims, role, space };
}

// ── CRUD: Channels ──

routes.get("/api/channels", async (c) => {
	if (!_syncServer) return c.json({ channels: [] });
	const space = c.req.param("space") || "demo";

	let callerRole: SpaceRoleString = 'viewer';
	const token = extractToken(c.req.raw.headers);
	if (token) {
		try {
			const claims = await verifyToken(token);
			const resolved = await resolveCallerRole(space, claims);
			if (resolved) callerRole = resolved.role;
		} catch {}
	}

	const doc = ensureDirectoryDoc(space);
	const visibleChannels = filterByVisibility(doc.channels || {}, callerRole);
	// Filter out DM channels from the public list
	const publicChannels = Object.values(visibleChannels).filter(ch => !ch.isDm);
	return c.json({ channels: publicChannels });
});

routes.post("/api/channels", async (c) => {
	const auth = await requireAuth(c);
	if (!auth) return c.res;
	const { claims, space } = auth;
	const { name, description = "", isPrivate = false } = await c.req.json();
	if (!name) return c.json({ error: "name required" }, 400);
	const id = crypto.randomUUID();
	const docId = chatsDirectoryDocId(space);
	ensureDirectoryDoc(space);
	_syncServer!.changeDoc<ChatsDirectoryDoc>(docId, `create channel ${id}`, (d) => {
		d.channels[id] = {
			id, name, description, isPrivate, isDm: false,
			createdBy: claims.did || claims.sub || null,
			createdAt: Date.now(), updatedAt: Date.now(),
		};
	});
	ensureChannelDoc(space, id);
	const updated = _syncServer!.getDoc<ChatsDirectoryDoc>(docId)!;
	return c.json(updated.channels[id], 201);
});

// ── CRUD: Messages ──

routes.get("/api/channels/:channelId/messages", async (c) => {
	if (!_syncServer) return c.json({ messages: [] });
	const space = c.req.param("space") || "demo";
	const channelId = c.req.param("channelId");

	let callerRole: SpaceRoleString = 'viewer';
	const token = extractToken(c.req.raw.headers);
	if (token) {
		try {
			const claims = await verifyToken(token);
			const resolved = await resolveCallerRole(space, claims);
			if (resolved) callerRole = resolved.role;
		} catch {}
	}

	const doc = ensureChannelDoc(space, channelId);
	// Only return top-level messages (not thread replies)
	const messages = filterArrayByVisibility(
		Object.values(doc.messages || {}).filter(m => !m.threadId),
		callerRole,
	).sort((a, b) => a.createdAt - b.createdAt);
	return c.json({ messages });
});

routes.post("/api/channels/:channelId/messages", async (c) => {
	const auth = await requireAuth(c);
	if (!auth) return c.res;
	const { claims, space } = auth;
	const channelId = c.req.param("channelId");
	const { content, replyTo = null, transclusions = [] } = await c.req.json();
	if (!content) return c.json({ error: "content required" }, 400);

	const id = crypto.randomUUID();
	const docId = chatChannelDocId(space, channelId);
	ensureChannelDoc(space, channelId);

	const authorDid = claims.did || claims.sub || '';
	const authorName = (claims.displayName as string) || claims.username || 'Anonymous';

	_syncServer!.changeDoc<ChatChannelDoc>(docId, `add message ${id}`, (d) => {
		d.messages[id] = {
			id, channelId,
			authorId: authorDid,
			authorName,
			content, replyTo,
			reactions: {}, transclusions: transclusions || [],
			editedAt: null, createdAt: Date.now(),
		};
	});

	// Notify channel members (skip sender)
	const updated = _syncServer!.getDoc<ChatChannelDoc>(docId)!;
	const memberDids = Object.values(updated.members || {})
		.map(m => m.userId).filter(did => did !== authorDid);
	for (const userDid of memberDids) {
		notify({
			userDid, category: 'module', eventType: 'chat_message',
			title: `${authorName} in #${channelId}`,
			body: content.slice(0, 200),
			spaceSlug: space, moduleId: 'rchats',
			actionUrl: `/${space}/rchats?channel=${channelId}`,
			actorDid: authorDid, actorUsername: authorName,
		}).catch(() => {});
	}

	// Notify @mentions
	const mentions = extractMentions(content);
	for (const mention of mentions) {
		// mentions are usernames — find DID from members
		const member = Object.values(updated.members || {}).find(m => m.displayName === mention);
		if (member && member.userId !== authorDid) {
			notify({
				userDid: member.userId, category: 'module', eventType: 'chat_mention',
				title: `${authorName} mentioned you in #${channelId}`,
				body: content.slice(0, 200),
				spaceSlug: space, moduleId: 'rchats',
				actionUrl: `/${space}/rchats?channel=${channelId}`,
				actorDid: authorDid, actorUsername: authorName,
			}).catch(() => {});
		}
	}

	return c.json(updated.messages[id], 201);
});

// ── Edit message ──

routes.put("/api/channels/:channelId/messages/:msgId", async (c) => {
	const auth = await requireAuth(c);
	if (!auth) return c.res;
	const { claims, space } = auth;
	const channelId = c.req.param("channelId");
	const msgId = c.req.param("msgId");
	const { content } = await c.req.json();
	if (!content) return c.json({ error: "content required" }, 400);

	const docId = chatChannelDocId(space, channelId);
	const doc = ensureChannelDoc(space, channelId);
	if (!doc.messages[msgId]) return c.json({ error: "Not found" }, 404);

	const authorDid = claims.did || claims.sub || '';
	if (doc.messages[msgId].authorId !== authorDid) {
		return c.json({ error: "Can only edit your own messages" }, 403);
	}

	_syncServer!.changeDoc<ChatChannelDoc>(docId, `edit message ${msgId}`, (d) => {
		d.messages[msgId].content = content;
		d.messages[msgId].editedAt = Date.now();
	});
	const updated = _syncServer!.getDoc<ChatChannelDoc>(docId)!;
	return c.json(updated.messages[msgId]);
});

// ── Delete message ──

routes.delete("/api/channels/:channelId/messages/:msgId", async (c) => {
	const auth = await requireAuth(c);
	if (!auth) return c.res;
	const { space } = auth;
	const channelId = c.req.param("channelId");
	const msgId = c.req.param("msgId");
	const docId = chatChannelDocId(space, channelId);
	const doc = ensureChannelDoc(space, channelId);
	if (!doc.messages[msgId]) return c.json({ error: "Not found" }, 404);
	_syncServer!.changeDoc<ChatChannelDoc>(docId, `delete message ${msgId}`, (d) => { delete d.messages[msgId]; });
	return c.json({ ok: true });
});

// ── Reactions ──

routes.post("/api/channels/:channelId/messages/:msgId/react", async (c) => {
	const auth = await requireAuth(c);
	if (!auth) return c.res;
	const { claims, space } = auth;
	const channelId = c.req.param("channelId");
	const msgId = c.req.param("msgId");
	const { emoji } = await c.req.json();
	if (!emoji) return c.json({ error: "emoji required" }, 400);

	const docId = chatChannelDocId(space, channelId);
	const doc = ensureChannelDoc(space, channelId);
	if (!doc.messages[msgId]) return c.json({ error: "Not found" }, 404);

	const userDid = claims.did || claims.sub || '';

	_syncServer!.changeDoc<ChatChannelDoc>(docId, `react ${emoji} on ${msgId}`, (d) => {
		const msg = d.messages[msgId];
		if (!msg.reactions) msg.reactions = {} as any;
		if (!msg.reactions[emoji]) msg.reactions[emoji] = [] as any;
		const existing = msg.reactions[emoji] as unknown as string[];
		const idx = existing.indexOf(userDid);
		if (idx >= 0) {
			existing.splice(idx, 1);
			if (existing.length === 0) delete msg.reactions[emoji];
		} else {
			existing.push(userDid);
		}
	});

	const updated = _syncServer!.getDoc<ChatChannelDoc>(docId)!;
	return c.json({ reactions: updated.messages[msgId]?.reactions || {} });
});

// ── Threads ──

routes.get("/api/channels/:channelId/threads/:threadId", async (c) => {
	if (!_syncServer) return c.json({ messages: [] });
	const space = c.req.param("space") || "demo";
	const channelId = c.req.param("channelId");
	const threadId = c.req.param("threadId");

	let callerRole: SpaceRoleString = 'viewer';
	const token = extractToken(c.req.raw.headers);
	if (token) {
		try {
			const claims = await verifyToken(token);
			const resolved = await resolveCallerRole(space, claims);
			if (resolved) callerRole = resolved.role;
		} catch {}
	}

	const doc = ensureChannelDoc(space, channelId);
	// Return root message + all replies
	const rootMsg = doc.messages[threadId];
	const replies = filterArrayByVisibility(
		Object.values(doc.messages || {}).filter(m => m.threadId === threadId),
		callerRole,
	).sort((a, b) => a.createdAt - b.createdAt);

	const thread = doc.threads?.[threadId] || null;
	return c.json({ rootMessage: rootMsg || null, replies, thread });
});

routes.post("/api/channels/:channelId/messages/:msgId/thread", async (c) => {
	const auth = await requireAuth(c);
	if (!auth) return c.res;
	const { claims, space } = auth;
	const channelId = c.req.param("channelId");
	const msgId = c.req.param("msgId");
	const { content, transclusions = [] } = await c.req.json();
	if (!content) return c.json({ error: "content required" }, 400);

	const docId = chatChannelDocId(space, channelId);
	const doc = ensureChannelDoc(space, channelId);
	if (!doc.messages[msgId]) return c.json({ error: "Root message not found" }, 404);

	const id = crypto.randomUUID();
	const authorDid = claims.did || claims.sub || '';
	const authorName = (claims.displayName as string) || claims.username || 'Anonymous';

	_syncServer!.changeDoc<ChatChannelDoc>(docId, `thread reply ${id}`, (d) => {
		d.messages[id] = {
			id, channelId,
			authorId: authorDid, authorName,
			content, replyTo: null, threadId: msgId,
			reactions: {}, transclusions: transclusions || [],
			editedAt: null, createdAt: Date.now(),
		};
		// Update thread metadata
		if (!d.threads) d.threads = {} as any;
		if (!d.threads[msgId]) {
			d.threads[msgId] = {
				participantDids: [] as any,
				lastActivity: Date.now(),
				replyCount: 0,
			};
		}
		const thread = d.threads[msgId];
		thread.lastActivity = Date.now();
		thread.replyCount = (thread.replyCount || 0) + 1;
		const participants = thread.participantDids as unknown as string[];
		if (!participants.includes(authorDid)) {
			participants.push(authorDid);
		}
	});

	// Notify thread participants (skip sender)
	const updated = _syncServer!.getDoc<ChatChannelDoc>(docId)!;
	const threadMeta = updated.threads?.[msgId];
	if (threadMeta) {
		const participantDids = (threadMeta.participantDids || []).filter((d: string) => d !== authorDid);
		// Also notify root message author
		const rootAuthor = updated.messages[msgId]?.authorId;
		if (rootAuthor && rootAuthor !== authorDid && !participantDids.includes(rootAuthor)) {
			participantDids.push(rootAuthor);
		}
		for (const userDid of participantDids) {
			notify({
				userDid, category: 'module', eventType: 'chat_message',
				title: `${authorName} replied in thread`,
				body: content.slice(0, 200),
				spaceSlug: space, moduleId: 'rchats',
				actionUrl: `/${space}/rchats?channel=${channelId}&thread=${msgId}`,
				actorDid: authorDid, actorUsername: authorName,
			}).catch(() => {});
		}
	}

	return c.json(updated.messages[id], 201);
});

// ── Pins ──

routes.post("/api/channels/:channelId/pins", async (c) => {
	const auth = await requireAuth(c);
	if (!auth) return c.res;
	const { space } = auth;
	const channelId = c.req.param("channelId");
	const { messageId } = await c.req.json();
	if (!messageId) return c.json({ error: "messageId required" }, 400);

	const docId = chatChannelDocId(space, channelId);
	const doc = ensureChannelDoc(space, channelId);
	if (!doc.messages[messageId]) return c.json({ error: "Message not found" }, 404);

	_syncServer!.changeDoc<ChatChannelDoc>(docId, `toggle pin ${messageId}`, (d) => {
		if (!d.pins) d.pins = [] as any;
		const pins = d.pins as unknown as string[];
		const idx = pins.indexOf(messageId);
		if (idx >= 0) {
			pins.splice(idx, 1);
		} else {
			pins.push(messageId);
		}
	});

	const updated = _syncServer!.getDoc<ChatChannelDoc>(docId)!;
	return c.json({ pins: updated.pins || [] });
});

// ── DMs ──

routes.get("/api/dm", async (c) => {
	const auth = await requireAuth(c);
	if (!auth) return c.res;
	const { claims, space } = auth;
	const myDid = claims.did || claims.sub || '';

	// Scan directory for DM channels involving this user
	const dir = ensureDirectoryDoc(space);
	const dmChannels = Object.values(dir.channels || {})
		.filter(ch => ch.isDm && ch.id.includes(myDid));

	return c.json({ channels: dmChannels });
});

routes.get("/api/dm/:targetDid", async (c) => {
	const auth = await requireAuth(c);
	if (!auth) return c.res;
	const { claims, space } = auth;
	const myDid = claims.did || claims.sub || '';
	const targetDid = c.req.param("targetDid");

	if (myDid === targetDid) return c.json({ error: "Cannot DM yourself" }, 400);

	const dmDocId = dmChannelDocId(space, myDid, targetDid);
	const channelId = `dm:${[myDid, targetDid].sort().join('+')}`;

	// Ensure DM channel exists in directory
	const dirDocId = chatsDirectoryDocId(space);
	ensureDirectoryDoc(space);
	const dir = _syncServer!.getDoc<ChatsDirectoryDoc>(dirDocId)!;
	if (!dir.channels[channelId]) {
		_syncServer!.changeDoc<ChatsDirectoryDoc>(dirDocId, `create DM ${channelId}`, (d) => {
			d.channels[channelId] = {
				id: channelId, name: `DM`, description: '',
				isPrivate: true, isDm: true, createdBy: myDid,
				createdAt: Date.now(), updatedAt: Date.now(),
			};
		});
	}

	// Ensure channel doc exists
	let doc = _syncServer!.getDoc<ChatChannelDoc>(dmDocId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<ChatChannelDoc>(), 'init DM channel', (d) => {
			const init = chatChannelSchema.init();
			Object.assign(d, init);
			d.meta.spaceSlug = space;
			d.channelId = channelId;
			d.isDm = true;
			d.members[myDid] = { userId: myDid, displayName: claims.displayName || claims.username || 'User', joinedAt: Date.now() };
			d.members[targetDid] = { userId: targetDid, displayName: targetDid, joinedAt: Date.now() };
		});
		_syncServer!.setDoc(dmDocId, doc);
	}

	return c.json({
		channelId,
		docId: dmDocId,
		messages: Object.values(doc.messages || {}).sort((a, b) => a.createdAt - b.createdAt),
		members: Object.values(doc.members || {}),
	});
});

routes.post("/api/dm/:targetDid/messages", async (c) => {
	const auth = await requireAuth(c);
	if (!auth) return c.res;
	const { claims, space } = auth;
	const myDid = claims.did || claims.sub || '';
	const targetDid = c.req.param("targetDid");
	const { content, transclusions = [] } = await c.req.json();
	if (!content) return c.json({ error: "content required" }, 400);

	const dmDocId = dmChannelDocId(space, myDid, targetDid);
	const channelId = `dm:${[myDid, targetDid].sort().join('+')}`;

	// Ensure DM exists first (reuse GET logic)
	let doc = _syncServer!.getDoc<ChatChannelDoc>(dmDocId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<ChatChannelDoc>(), 'init DM channel', (d) => {
			const init = chatChannelSchema.init();
			Object.assign(d, init);
			d.meta.spaceSlug = space;
			d.channelId = channelId;
			d.isDm = true;
		});
		_syncServer!.setDoc(dmDocId, doc);
	}

	const id = crypto.randomUUID();
	const authorName = (claims.displayName as string) || claims.username || 'Anonymous';

	_syncServer!.changeDoc<ChatChannelDoc>(dmDocId, `DM message ${id}`, (d) => {
		d.messages[id] = {
			id, channelId,
			authorId: myDid, authorName,
			content, replyTo: null,
			reactions: {}, transclusions: transclusions || [],
			editedAt: null, createdAt: Date.now(),
		};
	});

	// Notify recipient
	notify({
		userDid: targetDid, category: 'module', eventType: 'chat_dm',
		title: `DM from ${authorName}`,
		body: content.slice(0, 200),
		spaceSlug: space, moduleId: 'rchats',
		actionUrl: `/${space}/rchats?dm=${myDid}`,
		actorDid: myDid, actorUsername: authorName,
	}).catch(() => {});

	const updated = _syncServer!.getDoc<ChatChannelDoc>(dmDocId)!;
	return c.json(updated.messages[id], 201);
});

// ── Members (join/leave) ──

routes.post("/api/channels/:channelId/join", async (c) => {
	const auth = await requireAuth(c);
	if (!auth) return c.res;
	const { claims, space } = auth;
	const channelId = c.req.param("channelId");
	const docId = chatChannelDocId(space, channelId);
	ensureChannelDoc(space, channelId);
	const userDid = claims.did || claims.sub || '';
	_syncServer!.changeDoc<ChatChannelDoc>(docId, `join ${userDid}`, (d) => {
		d.members[userDid] = {
			userId: userDid,
			displayName: (claims.displayName as string) || claims.username || 'Anonymous',
			joinedAt: Date.now(),
		};
	});
	return c.json({ ok: true });
});

routes.post("/api/channels/:channelId/leave", async (c) => {
	const auth = await requireAuth(c);
	if (!auth) return c.res;
	const { claims, space } = auth;
	const channelId = c.req.param("channelId");
	const docId = chatChannelDocId(space, channelId);
	const userDid = claims.did || claims.sub || '';
	_syncServer!.changeDoc<ChatChannelDoc>(docId, `leave ${userDid}`, (d) => {
		delete d.members[userDid];
	});
	return c.json({ ok: true });
});

// ── Unread count ──

routes.get("/api/unread-count", async (c) => {
	if (!_syncServer) return c.json({ count: 0, channels: {} });
	const space = c.req.param("space") || "demo";
	const sinceParam = c.req.query("since");
	const since = sinceParam ? parseInt(sinceParam, 10) : 0;

	const dir = ensureDirectoryDoc(space);
	const result: Record<string, number> = {};
	let total = 0;

	for (const ch of Object.values(dir.channels || {})) {
		if (ch.isDm) continue;
		const doc = _syncServer!.getDoc<ChatChannelDoc>(chatChannelDocId(space, ch.id));
		if (!doc?.messages) continue;
		const count = Object.values(doc.messages).filter(m => !m.threadId && m.createdAt > since).length;
		if (count > 0) {
			result[ch.id] = count;
			total += count;
		}
	}

	return c.json({ count: total, channels: result });
});

// ── Hub page (active chat UI) ──

routes.get("/", (c) => {
	const space = c.req.param("space") || "demo";
	return c.html(renderShell({
		title: `rChats — ${space} | rSpace`,
		moduleId: "rchats",
		spaceSlug: space,
		modules: getModuleInfoList(),
		body: `<folk-chat-app space="${space}"></folk-chat-app>`,
		scripts: `<script type="module" src="/modules/rchats/folk-chat-app.js?v=1"></script>`,
	}));
});

// ── MI Integration ──

export function getRecentMessagesForMI(space: string, limit = 5): { id: string; channel: string; author: string; content: string; createdAt: number }[] {
	if (!_syncServer) return [];
	const all: { id: string; channel: string; author: string; content: string; createdAt: number }[] = [];
	for (const docId of _syncServer.listDocs()) {
		if (!docId.startsWith(`${space}:chats:channel:`)) continue;
		const doc = _syncServer.getDoc<ChatChannelDoc>(docId);
		if (!doc?.messages) continue;
		for (const msg of Object.values(doc.messages)) {
			all.push({ id: msg.id, channel: msg.channelId, author: msg.authorName, content: msg.content.slice(0, 200), createdAt: msg.createdAt });
		}
	}
	return all.sort((a, b) => b.createdAt - a.createdAt).slice(0, limit);
}

// ── Module export ──

export const chatsModule: RSpaceModule = {
	id: "rchats",
	name: "rChats",
	icon: "\u{1F4AC}",
	description: "Encrypted community messaging",
	scoping: { defaultScope: "space", userConfigurable: false },
	docSchemas: [
		{ pattern: '{space}:chats:channels', description: 'Channel directory per space', init: chatsDirectorySchema.init },
		{ pattern: '{space}:chats:channel:{channelId}', description: 'Messages per channel', init: chatChannelSchema.init },
		{ pattern: '{space}:chats:dm:{did1}+{did2}', description: 'DM channel', init: chatChannelSchema.init },
	],
	routes,
	landingPage: renderLanding,
	async onInit(ctx) { _syncServer = ctx.syncServer; },
	async onSpaceCreate(ctx: SpaceLifecycleContext) {
		if (!_syncServer) return;
		seedGeneralChannel(ctx.spaceSlug);
	},
};
