/**
 * rFeeds module — community RSS dashboard.
 *
 * Subscribe to external RSS/Atom feeds, write manual posts,
 * ingest rApp activity via adapters, manage user feed profiles,
 * expose combined activity as public Atom 1.0 feed.
 */

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 type { SyncServer } from "../../server/local-first/sync-server";
import { verifyToken, extractToken } from "../../server/auth";
import type { EncryptIDClaims } from "../../server/auth";
import { resolveCallerRole, roleAtLeast } from "../../server/spaces";
import type { SpaceRoleString } from "../../server/spaces";
import { feedsSchema, feedsDocId, MAX_ITEMS_PER_SOURCE, MAX_ACTIVITY_CACHE, DEFAULT_SYNC_INTERVAL_MS } from "./schemas";
import type { FeedsDoc, FeedSource, FeedItem, ManualPost, ActivityCacheEntry, ActivityModuleConfig, UserFeedProfile } from "./schemas";
import { parseFeed, parseOPML } from "./lib/feed-parser";
import { adapterRegistry } from "./adapters/index";
import { renderLanding } from "./landing";

let _syncServer: SyncServer | null = null;

const routes = new Hono();

// ── Automerge doc management ──

function ensureDoc(space: string): FeedsDoc {
	const docId = feedsDocId(space);
	let doc = _syncServer!.getDoc<FeedsDoc>(docId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<FeedsDoc>(), "init rfeeds", (d) => {
			const init = feedsSchema.init();
			d.meta = init.meta;
			d.meta.spaceSlug = space;
			d.sources = {};
			d.items = {};
			d.posts = {};
			d.activityCache = {};
			d.userProfiles = {};
			d.settings = init.settings;
		});
		_syncServer!.setDoc(docId, doc);
	}
	// Migrate v1 docs missing new fields
	if (!doc.activityCache || !doc.userProfiles || !doc.settings.activityModules) {
		_syncServer!.changeDoc<FeedsDoc>(docId, "migrate to v2", (d) => {
			if (!d.activityCache) d.activityCache = {} as any;
			if (!d.userProfiles) d.userProfiles = {} as any;
			if (!d.settings.activityModules) d.settings.activityModules = {} as any;
			if (d.meta) d.meta.version = 2;
		});
		doc = _syncServer!.getDoc<FeedsDoc>(docId)!;
	}
	return doc;
}

// ── Auth helpers ──

async function requireMember(c: any, space: string): Promise<{ claims: EncryptIDClaims; role: SpaceRoleString } | null> {
	const token = extractToken(c.req.raw);
	if (!token) { c.json({ error: "Unauthorized" }, 401); return null; }
	let claims: EncryptIDClaims;
	try { claims = await verifyToken(token); } catch { c.json({ error: "Invalid token" }, 401); return null; }
	const resolved = await resolveCallerRole(space, claims);
	if (!resolved || !roleAtLeast(resolved.role, "member")) { c.json({ error: "Forbidden" }, 403); return null; }
	return { claims, role: resolved.role };
}

async function requireAuth(c: any): Promise<EncryptIDClaims | null> {
	const token = extractToken(c.req.raw);
	if (!token) { c.json({ error: "Unauthorized" }, 401); return null; }
	try { return await verifyToken(token); } catch { c.json({ error: "Invalid token" }, 401); return null; }
}

// ── Fetch + sync a single source ──

async function syncSource(space: string, sourceId: string): Promise<{ added: number; error?: string }> {
	const doc = ensureDoc(space);
	const source = doc.sources[sourceId];
	if (!source) return { added: 0, error: "Source not found" };

	try {
		const res = await fetch(source.url, {
			signal: AbortSignal.timeout(15_000),
			headers: { "User-Agent": "rSpace-Feeds/1.0" },
		});
		if (!res.ok) throw new Error(`HTTP ${res.status}`);
		const xml = await res.text();
		const parsed = parseFeed(xml);

		// Collect existing guids for this source
		const existingGuids = new Set<string>();
		for (const item of Object.values(doc.items)) {
			if (item.sourceId === sourceId) existingGuids.add(item.guid);
		}

		// Filter new items
		const newItems = parsed.items.filter((i) => !existingGuids.has(i.guid));
		if (newItems.length === 0) {
			// Update lastFetchedAt only
			_syncServer!.changeDoc<FeedsDoc>(feedsDocId(space), "sync: no new items", (d) => {
				if (d.sources[sourceId]) {
					d.sources[sourceId].lastFetchedAt = Date.now();
					d.sources[sourceId].lastError = null;
				}
			});
			return { added: 0 };
		}

		_syncServer!.changeDoc<FeedsDoc>(feedsDocId(space), `sync: add ${newItems.length} items`, (d) => {
			for (const item of newItems) {
				const id = crypto.randomUUID();
				d.items[id] = {
					id,
					sourceId,
					guid: item.guid,
					title: item.title,
					url: item.url,
					summary: item.summary,
					author: item.author,
					publishedAt: item.publishedAt,
					importedAt: Date.now(),
					reshared: false,
				};
			}
			if (d.sources[sourceId]) {
				d.sources[sourceId].lastFetchedAt = Date.now();
				d.sources[sourceId].lastError = null;
			}
		});

		// Prune excess items per source
		pruneSourceItems(space, sourceId);

		// Update item count
		const updatedDoc = _syncServer!.getDoc<FeedsDoc>(feedsDocId(space))!;
		const count = Object.values(updatedDoc.items).filter((i) => i.sourceId === sourceId).length;
		_syncServer!.changeDoc<FeedsDoc>(feedsDocId(space), "update item count", (d) => {
			if (d.sources[sourceId]) d.sources[sourceId].itemCount = count;
		});

		return { added: newItems.length };
	} catch (err: any) {
		_syncServer!.changeDoc<FeedsDoc>(feedsDocId(space), "sync error", (d) => {
			if (d.sources[sourceId]) {
				d.sources[sourceId].lastFetchedAt = Date.now();
				d.sources[sourceId].lastError = err.message || "Unknown error";
			}
		});
		return { added: 0, error: err.message };
	}
}

function pruneSourceItems(space: string, sourceId: string) {
	const doc = _syncServer!.getDoc<FeedsDoc>(feedsDocId(space))!;
	const sourceItems = Object.values(doc.items)
		.filter((i) => i.sourceId === sourceId)
		.sort((a, b) => b.publishedAt - a.publishedAt);

	if (sourceItems.length <= MAX_ITEMS_PER_SOURCE) return;

	const toRemove = sourceItems.slice(MAX_ITEMS_PER_SOURCE);
	_syncServer!.changeDoc<FeedsDoc>(feedsDocId(space), "prune old items", (d) => {
		for (const item of toRemove) {
			delete d.items[item.id];
		}
	});
}

// ── Activity sync (Phase 2) ──

function getBaseUrl(): string {
	return process.env.BASE_URL || 'https://rspace.online';
}

function resolveModuleConfig(doc: FeedsDoc, moduleId: string): ActivityModuleConfig {
	const saved = doc.settings.activityModules?.[moduleId];
	if (saved) return saved;
	const entry = adapterRegistry[moduleId];
	return entry?.defaults || { enabled: false, label: moduleId, color: '#94a3b8' };
}

function runActivitySync(space: string, baseUrl: string): { synced: number; modules: string[] } {
	if (!_syncServer) return { synced: 0, modules: [] };
	const doc = ensureDoc(space);
	const since = Date.now() - 7 * 24 * 60 * 60 * 1000; // 7 days

	// Collect existing reshared flags to preserve
	const resharedFlags = new Map<string, boolean>();
	if (doc.activityCache) {
		for (const [id, entry] of Object.entries(doc.activityCache)) {
			if (entry.reshared) resharedFlags.set(id, true);
		}
	}

	const allEntries: ActivityCacheEntry[] = [];
	const syncedModules: string[] = [];

	for (const [moduleId, { adapter }] of Object.entries(adapterRegistry)) {
		const config = resolveModuleConfig(doc, moduleId);
		if (!config.enabled) continue;

		try {
			const entries = adapter({
				syncServer: _syncServer,
				space,
				since,
				baseUrl,
			});
			// Preserve reshared flags
			for (const entry of entries) {
				if (resharedFlags.has(entry.id)) entry.reshared = true;
			}
			allEntries.push(...entries);
			syncedModules.push(moduleId);
		} catch (err) {
			console.error(`[rFeeds] Activity adapter ${moduleId} failed for ${space}:`, err);
		}
	}

	if (allEntries.length === 0 && syncedModules.length === 0) return { synced: 0, modules: [] };

	// Sort by time, keep newest up to cap
	allEntries.sort((a, b) => b.publishedAt - a.publishedAt);
	const capped = allEntries.slice(0, MAX_ACTIVITY_CACHE);

	_syncServer!.changeDoc<FeedsDoc>(feedsDocId(space), `activity sync: ${capped.length} entries`, (d) => {
		// Replace cache entirely with fresh data
		d.activityCache = {} as any;
		for (const entry of capped) {
			d.activityCache[entry.id] = entry;
		}
	});

	return { synced: capped.length, modules: syncedModules };
}

// ── Background sync loop ──

const SYNC_INTERVAL_MS = 5 * 60 * 1000;
let _syncTimer: ReturnType<typeof setInterval> | null = null;

async function runBackgroundSync() {
	if (!_syncServer) return;
	const docIds = _syncServer.listDocs().filter((id: string) => id.endsWith(':rfeeds:data'));
	const baseUrl = getBaseUrl();

	for (const docId of docIds) {
		const space = docId.split(':')[0];
		const doc = _syncServer.getDoc<FeedsDoc>(docId);
		if (!doc?.sources) continue;

		// RSS source sync
		for (const source of Object.values(doc.sources)) {
			if (!source.enabled) continue;
			const interval = source.intervalMs || DEFAULT_SYNC_INTERVAL_MS;
			if (source.lastFetchedAt && source.lastFetchedAt + interval > Date.now()) continue;

			try {
				await syncSource(space, source.id);
			} catch (err) {
				console.error(`[rFeeds] Background sync failed for ${space}/${source.name}:`, err);
			}
		}

		// Activity adapter sync
		try {
			runActivitySync(space, baseUrl);
		} catch (err) {
			console.error(`[rFeeds] Activity sync failed for ${space}:`, err);
		}
	}
}

function startSyncLoop() {
	if (_syncTimer) return;
	// Initial sync after 45s startup delay
	setTimeout(() => {
		runBackgroundSync().catch((err) => console.error('[rFeeds] Background sync error:', err));
	}, 45_000);
	_syncTimer = setInterval(() => {
		runBackgroundSync().catch((err) => console.error('[rFeeds] Background sync error:', err));
	}, SYNC_INTERVAL_MS);
}

// ── Timeline helper ──

interface TimelineEntry {
	type: 'item' | 'post' | 'activity';
	id: string;
	title: string;
	url: string;
	summary: string;
	author: string;
	sourceName: string;
	sourceColor: string;
	publishedAt: number;
	reshared: boolean;
	moduleId?: string;
	itemType?: string;
}

function buildTimeline(doc: FeedsDoc, limit: number, before?: number): TimelineEntry[] {
	const entries: TimelineEntry[] = [];

	for (const item of Object.values(doc.items)) {
		const source = doc.sources[item.sourceId];
		if (!source?.enabled) continue;
		entries.push({
			type: 'item',
			id: item.id,
			title: item.title,
			url: item.url,
			summary: item.summary,
			author: item.author,
			sourceName: source?.name || 'Unknown',
			sourceColor: source?.color || '#94a3b8',
			publishedAt: item.publishedAt,
			reshared: item.reshared,
		});
	}

	for (const post of Object.values(doc.posts)) {
		entries.push({
			type: 'post',
			id: post.id,
			title: '',
			url: '',
			summary: post.content,
			author: post.authorName || post.authorDid,
			sourceName: 'Manual Post',
			sourceColor: '#67e8f9',
			publishedAt: post.createdAt,
			reshared: true, // manual posts always in atom feed
		});
	}

	// Activity cache entries
	if (doc.activityCache) {
		for (const entry of Object.values(doc.activityCache)) {
			const config = resolveModuleConfig(doc, entry.moduleId);
			if (!config.enabled) continue;
			entries.push({
				type: 'activity',
				id: entry.id,
				title: entry.title,
				url: entry.url,
				summary: entry.summary,
				author: entry.author,
				sourceName: config.label,
				sourceColor: config.color,
				publishedAt: entry.publishedAt,
				reshared: entry.reshared,
				moduleId: entry.moduleId,
				itemType: entry.itemType,
			});
		}
	}

	entries.sort((a, b) => b.publishedAt - a.publishedAt);

	if (before) {
		const idx = entries.findIndex((e) => e.publishedAt < before);
		if (idx === -1) return [];
		return entries.slice(idx, idx + limit);
	}

	return entries.slice(0, limit);
}

// ── Atom 1.0 generator ──

function escapeXml(s: string): string {
	return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}

function generateAtom(space: string, doc: FeedsDoc, baseUrl: string): string {
	const title = doc.settings.feedTitle || `${space} — rFeeds`;
	const description = doc.settings.feedDescription || `Community feed for ${space}`;
	const feedUrl = `${baseUrl}/${space}/rfeeds/feed.xml`;
	const siteUrl = baseUrl;

	// Collect reshared items + all manual posts + reshared activity
	const entries: { id: string; title: string; url: string; summary: string; author: string; publishedAt: number }[] = [];

	for (const item of Object.values(doc.items)) {
		if (!item.reshared) continue;
		entries.push(item);
	}
	for (const post of Object.values(doc.posts)) {
		entries.push({
			id: post.id,
			title: `Post by ${post.authorName || 'member'}`,
			url: '',
			summary: post.content,
			author: post.authorName || post.authorDid,
			publishedAt: post.createdAt,
		});
	}
	// Include reshared activity cache entries
	if (doc.activityCache) {
		for (const entry of Object.values(doc.activityCache)) {
			if (!entry.reshared) continue;
			entries.push({
				id: entry.id,
				title: entry.title,
				url: entry.url,
				summary: entry.summary,
				author: entry.author,
				publishedAt: entry.publishedAt,
			});
		}
	}

	entries.sort((a, b) => b.publishedAt - a.publishedAt);
	const latest = entries[0]?.publishedAt || Date.now();

	const atomEntries = entries.slice(0, 100).map((e) => `  <entry>
    <id>urn:rspace:${escapeXml(space)}:${escapeXml(e.id)}</id>
    <title>${escapeXml(e.title)}</title>
    <summary>${escapeXml(e.summary)}</summary>
    ${e.url ? `<link href="${escapeXml(e.url)}" rel="alternate"/>` : ''}
    <author><name>${escapeXml(e.author)}</name></author>
    <updated>${new Date(e.publishedAt).toISOString()}</updated>
  </entry>`).join('\n');

	return `<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>${escapeXml(title)}</title>
  <subtitle>${escapeXml(description)}</subtitle>
  <link href="${escapeXml(feedUrl)}" rel="self" type="application/atom+xml"/>
  <link href="${escapeXml(siteUrl)}" rel="alternate"/>
  <id>urn:rspace:${escapeXml(space)}:rfeeds</id>
  <updated>${new Date(latest).toISOString()}</updated>
  <generator>rSpace rFeeds</generator>
${atomEntries}
</feed>`;
}

// ── User feed Atom generator ──

function generateUserAtom(space: string, did: string, profile: UserFeedProfile, entries: ActivityCacheEntry[], baseUrl: string): string {
	const title = `${profile.displayName || did.slice(0, 16)} — rFeeds`;
	const description = profile.bio || `Personal feed for ${profile.displayName || did}`;
	const feedUrl = `${baseUrl}/${space}/rfeeds/user/${encodeURIComponent(did)}/feed.xml`;
	const siteUrl = baseUrl;

	entries.sort((a, b) => b.publishedAt - a.publishedAt);
	const latest = entries[0]?.publishedAt || Date.now();

	const atomEntries = entries.slice(0, 100).map((e) => `  <entry>
    <id>urn:rspace:${escapeXml(space)}:user:${escapeXml(did)}:${escapeXml(e.id)}</id>
    <title>${escapeXml(e.title)}</title>
    <summary>${escapeXml(e.summary)}</summary>
    ${e.url ? `<link href="${escapeXml(e.url)}" rel="alternate"/>` : ''}
    <author><name>${escapeXml(e.author || profile.displayName || did)}</name></author>
    <updated>${new Date(e.publishedAt).toISOString()}</updated>
  </entry>`).join('\n');

	return `<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>${escapeXml(title)}</title>
  <subtitle>${escapeXml(description)}</subtitle>
  <link href="${escapeXml(feedUrl)}" rel="self" type="application/atom+xml"/>
  <link href="${escapeXml(siteUrl)}" rel="alternate"/>
  <id>urn:rspace:${escapeXml(space)}:user:${escapeXml(did)}</id>
  <updated>${new Date(latest).toISOString()}</updated>
  <generator>rSpace rFeeds</generator>
${atomEntries}
</feed>`;
}

// ── Routes ──

// Page route
routes.get("/", (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c as any).get("effectiveSpace") || space;
	ensureDoc(dataSpace);

	return c.html(renderShell({
		title: `rFeeds — ${space}`,
		moduleId: "rfeeds",
		spaceSlug: space,
		body: `<folk-feeds-dashboard space="${dataSpace}"></folk-feeds-dashboard>`,
		scripts: `<script type="module" src="/modules/rfeeds/folk-feeds-dashboard.js"></script>`,
		styles: `<link rel="stylesheet" href="/modules/rfeeds/feeds.css">`,
		modules: getModuleInfoList(),
	}));
});

// Atom feed (public, no auth)
routes.get("/feed.xml", (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c as any).get("effectiveSpace") || space;
	const doc = ensureDoc(dataSpace);
	const proto = c.req.header("x-forwarded-proto") || "https";
	const host = c.req.header("x-forwarded-host") || c.req.header("host") || "rspace.online";
	const baseUrl = `${proto}://${host}`;

	c.header("Content-Type", "application/atom+xml; charset=utf-8");
	c.header("Cache-Control", "public, max-age=300");
	return c.body(generateAtom(space, doc, baseUrl));
});

// Timeline (public read)
routes.get("/api/timeline", (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c as any).get("effectiveSpace") || space;
	const doc = ensureDoc(dataSpace);
	const limit = Math.min(parseInt(c.req.query("limit") || "50"), 200);
	const before = c.req.query("before") ? parseInt(c.req.query("before")!) : undefined;

	return c.json(buildTimeline(doc, limit, before));
});

// Sources (public read)
routes.get("/api/sources", (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c as any).get("effectiveSpace") || space;
	const doc = ensureDoc(dataSpace);
	const sources = Object.values(doc.sources).sort((a, b) => b.addedAt - a.addedAt);
	return c.json(sources);
});

// Add source (member+)
routes.post("/api/sources", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c as any).get("effectiveSpace") || space;
	const auth = await requireMember(c, dataSpace);
	if (!auth) return;

	const body = await c.req.json<{ url: string; name?: string; color?: string }>();
	if (!body.url) return c.json({ error: "url required" }, 400);

	// Validate URL
	try { new URL(body.url); } catch { return c.json({ error: "Invalid URL" }, 400); }

	const id = crypto.randomUUID();
	const source: FeedSource = {
		id,
		url: body.url,
		name: body.name || new URL(body.url).hostname,
		color: body.color || '#94a3b8',
		enabled: true,
		lastFetchedAt: 0,
		lastError: null,
		itemCount: 0,
		intervalMs: DEFAULT_SYNC_INTERVAL_MS,
		addedBy: (auth.claims.did as string) || (auth.claims.sub as string),
		addedAt: Date.now(),
	};

	_syncServer!.changeDoc<FeedsDoc>(feedsDocId(dataSpace), "add feed source", (d) => {
		d.sources[id] = source;
	});

	// Trigger initial sync in background
	syncSource(dataSpace, id).catch(() => {});

	return c.json(source, 201);
});

// Update source (member+)
routes.put("/api/sources/:id", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c as any).get("effectiveSpace") || space;
	const auth = await requireMember(c, dataSpace);
	if (!auth) return;

	const sourceId = c.req.param("id");
	const body = await c.req.json<{ name?: string; color?: string; enabled?: boolean; intervalMs?: number }>();

	const doc = ensureDoc(dataSpace);
	if (!doc.sources[sourceId]) return c.json({ error: "Not found" }, 404);

	_syncServer!.changeDoc<FeedsDoc>(feedsDocId(dataSpace), "update feed source", (d) => {
		const s = d.sources[sourceId];
		if (!s) return;
		if (body.name !== undefined) s.name = body.name;
		if (body.color !== undefined) s.color = body.color;
		if (body.enabled !== undefined) s.enabled = body.enabled;
		if (body.intervalMs !== undefined) s.intervalMs = body.intervalMs;
	});

	const updated = _syncServer!.getDoc<FeedsDoc>(feedsDocId(dataSpace))!;
	return c.json(updated.sources[sourceId]);
});

// Delete source (member+)
routes.delete("/api/sources/:id", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c as any).get("effectiveSpace") || space;
	const auth = await requireMember(c, dataSpace);
	if (!auth) return;

	const sourceId = c.req.param("id");
	const doc = ensureDoc(dataSpace);
	if (!doc.sources[sourceId]) return c.json({ error: "Not found" }, 404);

	// Remove source + its items
	_syncServer!.changeDoc<FeedsDoc>(feedsDocId(dataSpace), "delete feed source", (d) => {
		delete d.sources[sourceId];
		for (const [itemId, item] of Object.entries(d.items)) {
			if (item.sourceId === sourceId) delete d.items[itemId];
		}
	});

	return c.json({ ok: true });
});

// Force sync one source (member+)
routes.post("/api/sources/:id/sync", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c as any).get("effectiveSpace") || space;
	const auth = await requireMember(c, dataSpace);
	if (!auth) return;

	const sourceId = c.req.param("id");
	const result = await syncSource(dataSpace, sourceId);
	return c.json(result);
});

// Import OPML (member+)
routes.post("/api/sources/import-opml", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c as any).get("effectiveSpace") || space;
	const auth = await requireMember(c, dataSpace);
	if (!auth) return;

	const body = await c.req.json<{ opml: string }>();
	if (!body.opml) return c.json({ error: "opml required" }, 400);

	let feeds: { url: string; name: string }[];
	try { feeds = parseOPML(body.opml); } catch (err: any) {
		return c.json({ error: `Invalid OPML: ${err.message}` }, 400);
	}

	const added: string[] = [];
	const did = (auth.claims.did as string) || (auth.claims.sub as string);

	for (const feed of feeds) {
		// Skip duplicates
		const doc = _syncServer!.getDoc<FeedsDoc>(feedsDocId(dataSpace))!;
		const exists = Object.values(doc.sources).some((s) => s.url === feed.url);
		if (exists) continue;

		const id = crypto.randomUUID();
		_syncServer!.changeDoc<FeedsDoc>(feedsDocId(dataSpace), `import: ${feed.name}`, (d) => {
			d.sources[id] = {
				id,
				url: feed.url,
				name: feed.name,
				color: '#94a3b8',
				enabled: true,
				lastFetchedAt: 0,
				lastError: null,
				itemCount: 0,
				intervalMs: DEFAULT_SYNC_INTERVAL_MS,
				addedBy: did,
				addedAt: Date.now(),
			};
		});
		added.push(feed.name);

		// Background sync each
		syncSource(dataSpace, id).catch(() => {});
	}

	return c.json({ added: added.length, names: added });
});

// Create manual post (member+)
routes.post("/api/posts", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c as any).get("effectiveSpace") || space;
	const auth = await requireMember(c, dataSpace);
	if (!auth) return;

	const body = await c.req.json<{ content: string }>();
	if (!body.content?.trim()) return c.json({ error: "content required" }, 400);

	const id = crypto.randomUUID();
	const did = (auth.claims.did as string) || (auth.claims.sub as string);

	_syncServer!.changeDoc<FeedsDoc>(feedsDocId(dataSpace), "create post", (d) => {
		d.posts[id] = {
			id,
			content: body.content.trim().slice(0, 2000),
			authorDid: did,
			authorName: (auth.claims as any).displayName || did.slice(0, 12),
			createdAt: Date.now(),
			updatedAt: Date.now(),
		};
	});

	const doc = _syncServer!.getDoc<FeedsDoc>(feedsDocId(dataSpace))!;
	return c.json(doc.posts[id], 201);
});

// Edit own post (author only)
routes.put("/api/posts/:id", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c as any).get("effectiveSpace") || space;
	const auth = await requireMember(c, dataSpace);
	if (!auth) return;

	const postId = c.req.param("id");
	const doc = ensureDoc(dataSpace);
	const post = doc.posts[postId];
	if (!post) return c.json({ error: "Not found" }, 404);

	const callerDid = (auth.claims.did as string) || (auth.claims.sub as string);
	if (post.authorDid !== callerDid) return c.json({ error: "Forbidden" }, 403);

	const body = await c.req.json<{ content: string }>();
	if (!body.content?.trim()) return c.json({ error: "content required" }, 400);

	_syncServer!.changeDoc<FeedsDoc>(feedsDocId(dataSpace), "edit post", (d) => {
		if (d.posts[postId]) {
			d.posts[postId].content = body.content.trim().slice(0, 2000);
			d.posts[postId].updatedAt = Date.now();
		}
	});

	const updated = _syncServer!.getDoc<FeedsDoc>(feedsDocId(dataSpace))!;
	return c.json(updated.posts[postId]);
});

// Delete post (author or admin)
routes.delete("/api/posts/:id", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c as any).get("effectiveSpace") || space;
	const auth = await requireMember(c, dataSpace);
	if (!auth) return;

	const postId = c.req.param("id");
	const doc = ensureDoc(dataSpace);
	const post = doc.posts[postId];
	if (!post) return c.json({ error: "Not found" }, 404);

	const callerDid = (auth.claims.did as string) || (auth.claims.sub as string);
	const isAuthor = post.authorDid === callerDid;
	const isAdmin = roleAtLeast(auth.role, "admin");
	if (!isAuthor && !isAdmin) return c.json({ error: "Forbidden" }, 403);

	_syncServer!.changeDoc<FeedsDoc>(feedsDocId(dataSpace), "delete post", (d) => {
		delete d.posts[postId];
	});

	return c.json({ ok: true });
});

// Toggle reshare on item (member+)
routes.patch("/api/items/:id/reshare", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c as any).get("effectiveSpace") || space;
	const auth = await requireMember(c, dataSpace);
	if (!auth) return;

	const itemId = c.req.param("id");
	const doc = ensureDoc(dataSpace);
	if (!doc.items[itemId]) return c.json({ error: "Not found" }, 404);

	const current = doc.items[itemId].reshared;
	_syncServer!.changeDoc<FeedsDoc>(feedsDocId(dataSpace), "toggle reshare", (d) => {
		if (d.items[itemId]) d.items[itemId].reshared = !current;
	});

	return c.json({ reshared: !current });
});

// ── Activity routes (Phase 2) ──

// Activity module config (public read)
routes.get("/api/activity/config", (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c as any).get("effectiveSpace") || space;
	const doc = ensureDoc(dataSpace);

	const config: Record<string, ActivityModuleConfig> = {};
	for (const moduleId of Object.keys(adapterRegistry)) {
		config[moduleId] = resolveModuleConfig(doc, moduleId);
	}
	return c.json(config);
});

// Update activity module config (admin)
routes.put("/api/activity/config", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c as any).get("effectiveSpace") || space;
	const auth = await requireMember(c, dataSpace);
	if (!auth) return;
	if (!roleAtLeast(auth.role, "admin")) return c.json({ error: "Admin required" }, 403);

	const body = await c.req.json<{ modules: Record<string, ActivityModuleConfig> }>();
	if (!body.modules) return c.json({ error: "modules required" }, 400);

	_syncServer!.changeDoc<FeedsDoc>(feedsDocId(dataSpace), "update activity config", (d) => {
		for (const [moduleId, config] of Object.entries(body.modules)) {
			if (!adapterRegistry[moduleId]) continue;
			d.settings.activityModules[moduleId] = config;
		}
	});

	const updated = _syncServer!.getDoc<FeedsDoc>(feedsDocId(dataSpace))!;
	const result: Record<string, ActivityModuleConfig> = {};
	for (const moduleId of Object.keys(adapterRegistry)) {
		result[moduleId] = resolveModuleConfig(updated, moduleId);
	}
	return c.json(result);
});

// Trigger on-demand activity sync (member+)
routes.post("/api/activity/sync", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c as any).get("effectiveSpace") || space;
	const auth = await requireMember(c, dataSpace);
	if (!auth) return;

	const baseUrl = getBaseUrl();
	const result = runActivitySync(dataSpace, baseUrl);
	return c.json(result);
});

// Toggle reshare on activity cache entry (member+)
routes.patch("/api/activity/:id/reshare", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c as any).get("effectiveSpace") || space;
	const auth = await requireMember(c, dataSpace);
	if (!auth) return;

	const entryId = decodeURIComponent(c.req.param("id"));
	const doc = ensureDoc(dataSpace);
	if (!doc.activityCache?.[entryId]) return c.json({ error: "Not found" }, 404);

	const current = doc.activityCache[entryId].reshared;
	_syncServer!.changeDoc<FeedsDoc>(feedsDocId(dataSpace), "toggle activity reshare", (d) => {
		if (d.activityCache[entryId]) d.activityCache[entryId].reshared = !current;
	});

	return c.json({ reshared: !current });
});

// ── User profile routes (Phase 2) ──

// Own profile (auth required)
routes.get("/api/profile", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c as any).get("effectiveSpace") || space;
	const claims = await requireAuth(c);
	if (!claims) return;

	const did = (claims.did as string) || (claims.sub as string);
	const doc = ensureDoc(dataSpace);
	const profile = doc.userProfiles?.[did] || null;
	return c.json(profile);
});

// Update own profile (auth required)
routes.put("/api/profile", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c as any).get("effectiveSpace") || space;
	const claims = await requireAuth(c);
	if (!claims) return;

	const did = (claims.did as string) || (claims.sub as string);
	const body = await c.req.json<{ displayName?: string; bio?: string; publishModules?: string[] }>();

	const doc = ensureDoc(dataSpace);
	const existing = doc.userProfiles?.[did];
	const now = Date.now();

	_syncServer!.changeDoc<FeedsDoc>(feedsDocId(dataSpace), "update user profile", (d) => {
		if (!d.userProfiles[did]) {
			d.userProfiles[did] = {
				did,
				displayName: body.displayName || (claims as any).displayName || did.slice(0, 16),
				bio: body.bio || '',
				publishModules: body.publishModules || [],
				createdAt: now,
				updatedAt: now,
			};
		} else {
			if (body.displayName !== undefined) d.userProfiles[did].displayName = body.displayName;
			if (body.bio !== undefined) d.userProfiles[did].bio = body.bio;
			if (body.publishModules !== undefined) d.userProfiles[did].publishModules = body.publishModules as any;
			d.userProfiles[did].updatedAt = now;
		}
	});

	const updated = _syncServer!.getDoc<FeedsDoc>(feedsDocId(dataSpace))!;
	return c.json(updated.userProfiles[did]);
});

// Read any user's profile (public)
routes.get("/api/profile/:did", (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c as any).get("effectiveSpace") || space;
	const did = decodeURIComponent(c.req.param("did"));
	const doc = ensureDoc(dataSpace);
	const profile = doc.userProfiles?.[did];
	if (!profile) return c.json({ error: "Profile not found" }, 404);
	return c.json(profile);
});

// Personal Atom feed (public)
routes.get("/user/:did/feed.xml", (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c as any).get("effectiveSpace") || space;
	const did = decodeURIComponent(c.req.param("did"));
	const doc = ensureDoc(dataSpace);
	const profile = doc.userProfiles?.[did];
	if (!profile) return c.text("Profile not found", 404);

	const proto = c.req.header("x-forwarded-proto") || "https";
	const host = c.req.header("x-forwarded-host") || c.req.header("host") || "rspace.online";
	const baseUrl = `${proto}://${host}`;

	// Collect activity entries matching user's published modules
	const publishSet = new Set(profile.publishModules || []);
	const entries: ActivityCacheEntry[] = [];

	if (doc.activityCache) {
		for (const entry of Object.values(doc.activityCache)) {
			if (!publishSet.has(entry.moduleId)) continue;
			entries.push(entry);
		}
	}

	// Also include user's manual posts as pseudo-entries
	if (publishSet.has('posts') || publishSet.size === 0) {
		for (const post of Object.values(doc.posts)) {
			if (post.authorDid !== did) continue;
			entries.push({
				id: `post:${post.id}`,
				moduleId: 'posts',
				itemType: 'post',
				title: `Post by ${post.authorName || 'member'}`,
				summary: post.content.slice(0, 280),
				url: '',
				author: post.authorName || post.authorDid,
				publishedAt: post.createdAt,
				reshared: true,
				syncedAt: post.updatedAt,
			});
		}
	}

	c.header("Content-Type", "application/atom+xml; charset=utf-8");
	c.header("Cache-Control", "public, max-age=300");
	return c.body(generateUserAtom(space, did, profile, entries, baseUrl));
});

// ── Module export ──

export const feedsModule: RSpaceModule = {
	id: "rfeeds",
	name: "rFeeds",
	icon: "\uD83D\uDCE1",
	description: "Community RSS dashboard — subscribe, curate, republish",
	routes,
	scoping: { defaultScope: "space", userConfigurable: false },
	docSchemas: [feedsSchema as any],
	standaloneDomain: "rfeeds.online",
	landingPage: renderLanding,

	onInit: async ({ syncServer }) => {
		_syncServer = syncServer;
		startSyncLoop();
	},

	onSpaceCreate: async ({ spaceSlug }) => {
		if (_syncServer) ensureDoc(spaceSlug);
	},
};
