/**
 * Socials module — federated social feed aggregator.
 *
 * Slim mod.ts: Automerge doc management, image API routes,
 * page routes (injecting web components), seed template, module export.
 *
 * All UI moved to web components in components/.
 * Thread/campaign CRUD handled by Automerge (no REST CRUD).
 * File-based threads migrated to Automerge on first access.
 */

import { resolve } from "node:path";
import { readdir, readFile } from "node:fs/promises";
import { Hono } from "hono";
import * as Automerge from "@automerge/automerge";
import { renderShell, renderExternalAppShell, escapeHtml, RICH_LANDING_CSS } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import type { SyncServer } from "../../server/local-first/sync-server";
import { renderLanding } from "./landing";
import { MYCOFI_CAMPAIGN, buildDemoCampaignFlow } from "./campaign-data";
import { socialsSchema, socialsDocId, type SocialsDoc, type ThreadData } from "./schemas";
import {
	generateImageFromPrompt,
	downloadAndSaveImage,
	validateImageFile,
	safeExtension,
	saveUploadedFile,
	deleteImageFile,
	deleteOldImage,
} from "./lib/image-gen";
import { DEMO_FEED } from "./lib/types";
import { getListmonkConfig, listmonkFetch } from "./lib/listmonk-proxy";
import { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import type { EncryptIDClaims } from "@encryptid/sdk/server";
import { resolveCallerRole, roleAtLeast } from "../../server/spaces";
import type { SpaceRoleString } from "../../server/spaces";

let _syncServer: SyncServer | null = null;

const routes = new Hono();

// ── Automerge doc management ──

function ensureDoc(space: string): SocialsDoc {
	const docId = socialsDocId(space);
	let doc = _syncServer!.getDoc<SocialsDoc>(docId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<SocialsDoc>(), "init", (d) => {
			const init = socialsSchema.init();
			d.meta = init.meta;
			d.meta.spaceSlug = space;
			d.threads = {};
			d.campaigns = {};
			d.campaignFlows = {};
			d.activeFlowId = '';
		});
		_syncServer!.setDoc(docId, doc);
	}
	return doc;
}

function getThreadFromDoc(space: string, id: string): ThreadData | undefined {
	const doc = ensureDoc(space);
	return doc.threads?.[id];
}

// ── Migration: file-based threads → Automerge ──

async function migrateFileThreadsToAutomerge(space: string): Promise<void> {
	const doc = ensureDoc(space);
	if (Object.keys(doc.threads || {}).length > 0) return; // Already has threads

	const threadsDir = resolve(process.env.FILES_DIR || "./data/files", "threads");
	let files: string[];
	try {
		files = await readdir(threadsDir);
	} catch {
		return; // No threads directory
	}

	let count = 0;
	const docId = socialsDocId(space);
	for (const f of files) {
		if (!f.endsWith(".json")) continue;
		try {
			const raw = await readFile(resolve(threadsDir, f), "utf-8");
			const thread: ThreadData = JSON.parse(raw);
			_syncServer!.changeDoc<SocialsDoc>(docId, `migrate thread ${thread.id}`, (d) => {
				if (!d.threads) d.threads = {} as any;
				d.threads[thread.id] = thread;
			});
			count++;
		} catch { /* skip corrupt files */ }
	}

	if (count > 0) {
		console.log(`[rSocials] Migrated ${count} file-based threads to Automerge for space "${space}"`);
	}
}

// ── Seed template ──

function seedTemplateSocials(space: string): void {
	if (!_syncServer) return;
	const doc = ensureDoc(space);

	// Seed MYCOFI_CAMPAIGN if no campaigns exist
	if (Object.keys(doc.campaigns || {}).length === 0) {
		const docId = socialsDocId(space);
		const now = Date.now();
		_syncServer.changeDoc<SocialsDoc>(docId, "seed campaign", (d) => {
			if (!d.campaigns) d.campaigns = {} as any;
			d.campaigns[MYCOFI_CAMPAIGN.id] = {
				...MYCOFI_CAMPAIGN,
				createdAt: now,
				updatedAt: now,
			};
		});
	}

	// Seed campaign flow if empty
	if (Object.keys(doc.campaignFlows || {}).length === 0) {
		const docId = socialsDocId(space);
		const flow = buildDemoCampaignFlow();
		_syncServer.changeDoc<SocialsDoc>(docId, "seed campaign flow", (d) => {
			if (!d.campaignFlows) d.campaignFlows = {} as any;
			d.campaignFlows[flow.id] = flow;
			d.activeFlowId = flow.id;
		});
	}

	// Seed a sample thread if empty
	if (Object.keys(doc.threads || {}).length === 0) {
		const docId = socialsDocId(space);
		const now = Date.now();
		const threadId = `t-${now}-seed`;
		_syncServer.changeDoc<SocialsDoc>(docId, "seed thread", (d) => {
			if (!d.threads) d.threads = {} as any;
			d.threads[threadId] = {
				id: threadId,
				name: "rSocials",
				handle: "@rsocials",
				title: "Welcome to Thread Editor",
				tweets: [
					"Welcome to the rSocials Thread Editor! Write your thread content here, separated by --- between tweets.",
					"Each section becomes a separate tweet card with live character counts and thread numbering.",
					"When you're ready, export to Twitter, Bluesky, Mastodon, or LinkedIn. All locally stored, no third-party data mining.",
				],
				createdAt: now,
				updatedAt: now,
			};
		});
		console.log(`[rSocials] Template seeded for "${space}": campaign + sample thread`);
	}
}

// ── API: Health & Info ──

routes.get("/api/health", (c) => c.json({ ok: true, module: "rsocials" }));

routes.get("/api/info", (c) =>
	c.json({
		module: "rsocials",
		description: "Federated social feed aggregator for communities",
		features: ["ActivityPub integration", "RSS feed aggregation", "Link sharing", "Community timeline"],
	}),
);

// ── API: Demo feed ──

routes.get("/api/feed", (c) =>
	c.json({
		items: [
			{ id: "demo-1", type: "post", author: "Alice", content: "Just published our community governance proposal!", source: "fediverse", timestamp: new Date(Date.now() - 3600_000).toISOString(), likes: 12, replies: 3 },
			{ id: "demo-2", type: "link", author: "Bob", content: "Great article on local-first collaboration", url: "https://example.com/local-first", source: "shared", timestamp: new Date(Date.now() - 7200_000).toISOString(), likes: 8, replies: 1 },
			{ id: "demo-3", type: "post", author: "Carol", content: "Welcome new members! Check out rSpace's tools in the app switcher above.", source: "local", timestamp: new Date(Date.now() - 14400_000).toISOString(), likes: 24, replies: 7 },
		],
		demo: true,
	}),
);

// ── Image API routes (server-side, need filesystem + FAL_KEY) ──

routes.post("/api/threads/:id/image", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c.get("effectiveSpace" as any) as string) || space;
	const id = c.req.param("id");
	if (!id || id.includes("..") || id.includes("/")) return c.json({ error: "Invalid ID" }, 400);

	const thread = getThreadFromDoc(dataSpace, id);
	if (!thread) return c.json({ error: "Thread not found" }, 404);

	if (!process.env.FAL_KEY) return c.json({ error: "FAL_KEY not configured" }, 503);

	const summary = thread.tweets.slice(0, 3).join(" ").substring(0, 300);
	const prompt = `Abstract artistic illustration evoking the mood and themes of: "${summary}". Style: cinematic dark atmosphere with deep navy and charcoal tones, luminous flowing gradients of electric blue, violet, and teal, organic abstract shapes and flowing forms, subtle geometric patterns, ethereal light rays and bokeh, no text, no words, no letters, no typography, no UI elements. Pure visual art — evocative, atmospheric, and beautiful. Landscape 4:3.`;

	let cdnUrl: string | null;
	try {
		cdnUrl = await generateImageFromPrompt(prompt);
	} catch (e: any) {
		console.error("[rSocials] Thread image generation error:", e.message);
		return c.json({ error: "Image generation failed: " + (e.message || "unknown error") }, 502);
	}
	if (!cdnUrl) return c.json({ error: "Image generation failed" }, 502);

	const filename = `thread-${id}.png`;
	let imageUrl: string | null;
	try {
		imageUrl = await downloadAndSaveImage(cdnUrl, filename);
	} catch (e: any) {
		console.error("[rSocials] Image download error:", e.message);
		return c.json({ error: "Failed to download image: " + (e.message || "unknown error") }, 502);
	}
	if (!imageUrl) return c.json({ error: "Failed to download image" }, 502);

	// Update Automerge doc with image URL
	const docId = socialsDocId(dataSpace);
	_syncServer!.changeDoc<SocialsDoc>(docId, "set thread image", (d) => {
		if (d.threads?.[id]) {
			d.threads[id].imageUrl = imageUrl;
			d.threads[id].updatedAt = Date.now();
		}
	});

	return c.json({ imageUrl });
});

routes.post("/api/threads/:id/upload-image", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c.get("effectiveSpace" as any) as string) || space;
	const id = c.req.param("id");
	if (!id || id.includes("..") || id.includes("/")) return c.json({ error: "Invalid ID" }, 400);

	const thread = getThreadFromDoc(dataSpace, id);
	if (!thread) return c.json({ error: "Thread not found" }, 404);

	let formData: FormData;
	try { formData = await c.req.formData(); } catch { return c.json({ error: "Invalid form data" }, 400); }

	const file = formData.get("file");
	if (!file || !(file instanceof File)) return c.json({ error: "No file provided" }, 400);

	const err = validateImageFile(file);
	if (err) return c.json({ error: err }, 400);

	const ext = safeExtension(file.name);
	const filename = `thread-${id}.${ext}`;

	if (thread.imageUrl) await deleteOldImage(thread.imageUrl, filename);

	const buffer = Buffer.from(await file.arrayBuffer());
	const imageUrl = await saveUploadedFile(buffer, filename);

	const docId = socialsDocId(dataSpace);
	_syncServer!.changeDoc<SocialsDoc>(docId, "upload thread image", (d) => {
		if (d.threads?.[id]) {
			d.threads[id].imageUrl = imageUrl;
			d.threads[id].updatedAt = Date.now();
		}
	});

	return c.json({ imageUrl });
});

routes.post("/api/threads/:id/tweet/:index/upload-image", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c.get("effectiveSpace" as any) as string) || space;
	const id = c.req.param("id");
	const index = c.req.param("index");
	if (!id || id.includes("..") || id.includes("/")) return c.json({ error: "Invalid ID" }, 400);
	if (!/^\d+$/.test(index)) return c.json({ error: "Invalid index" }, 400);

	const thread = getThreadFromDoc(dataSpace, id);
	if (!thread) return c.json({ error: "Thread not found" }, 404);

	let formData: FormData;
	try { formData = await c.req.formData(); } catch { return c.json({ error: "Invalid form data" }, 400); }

	const file = formData.get("file");
	if (!file || !(file instanceof File)) return c.json({ error: "No file provided" }, 400);

	const err = validateImageFile(file);
	if (err) return c.json({ error: err }, 400);

	const ext = safeExtension(file.name);
	const filename = `thread-${id}-tweet-${index}.${ext}`;

	const oldUrl = thread.tweetImages?.[index];
	if (oldUrl) await deleteOldImage(oldUrl, filename);

	const buffer = Buffer.from(await file.arrayBuffer());
	const imageUrl = await saveUploadedFile(buffer, filename);

	const docId = socialsDocId(dataSpace);
	_syncServer!.changeDoc<SocialsDoc>(docId, "upload tweet image", (d) => {
		if (d.threads?.[id]) {
			if (!d.threads[id].tweetImages) d.threads[id].tweetImages = {} as any;
			d.threads[id].tweetImages![index] = imageUrl;
			d.threads[id].updatedAt = Date.now();
		}
	});

	return c.json({ imageUrl });
});

routes.post("/api/threads/:id/tweet/:index/image", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c.get("effectiveSpace" as any) as string) || space;
	const id = c.req.param("id");
	const index = c.req.param("index");
	if (!id || id.includes("..") || id.includes("/")) return c.json({ error: "Invalid ID" }, 400);
	if (!/^\d+$/.test(index)) return c.json({ error: "Invalid index" }, 400);

	const thread = getThreadFromDoc(dataSpace, id);
	if (!thread) return c.json({ error: "Thread not found" }, 404);

	const tweetIndex = parseInt(index, 10);
	if (tweetIndex < 0 || tweetIndex >= thread.tweets.length) {
		return c.json({ error: "Tweet index out of range" }, 400);
	}

	if (!process.env.FAL_KEY) return c.json({ error: "FAL_KEY not configured" }, 503);

	const tweetText = thread.tweets[tweetIndex].substring(0, 300);
	const prompt = `Abstract artistic illustration inspired by the theme: "${tweetText}". Style: moody cinematic lighting on dark background, luminous abstract forms with flowing gradients of electric blue, violet, and warm amber, organic shapes suggesting the concept without being literal, subtle particle effects and light diffusion, no text, no words, no letters, no numbers, no typography whatsoever. Pure atmospheric visual art — stylistic, evocative, gallery-quality. Landscape 4:3.`;

	let cdnUrl: string | null;
	try {
		cdnUrl = await generateImageFromPrompt(prompt);
	} catch (e: any) {
		console.error("[rSocials] Tweet image generation error:", e.message);
		return c.json({ error: "Image generation failed: " + (e.message || "unknown error") }, 502);
	}
	if (!cdnUrl) return c.json({ error: "Image generation failed" }, 502);

	const filename = `thread-${id}-tweet-${index}.png`;

	const oldUrl = thread.tweetImages?.[index];
	if (oldUrl) await deleteOldImage(oldUrl, filename);

	let imageUrl: string | null;
	try {
		imageUrl = await downloadAndSaveImage(cdnUrl, filename);
	} catch (e: any) {
		console.error("[rSocials] Image download error:", e.message);
		return c.json({ error: "Failed to download image: " + (e.message || "unknown error") }, 502);
	}
	if (!imageUrl) return c.json({ error: "Failed to download image" }, 502);

	const docId = socialsDocId(dataSpace);
	_syncServer!.changeDoc<SocialsDoc>(docId, "generate tweet image", (d) => {
		if (d.threads?.[id]) {
			if (!d.threads[id].tweetImages) d.threads[id].tweetImages = {} as any;
			d.threads[id].tweetImages![index] = imageUrl;
			d.threads[id].updatedAt = Date.now();
		}
	});

	return c.json({ imageUrl });
});

routes.delete("/api/threads/:id/tweet/:index/image", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c.get("effectiveSpace" as any) as string) || space;
	const id = c.req.param("id");
	const index = c.req.param("index");
	if (!id || id.includes("..") || id.includes("/")) return c.json({ error: "Invalid ID" }, 400);
	if (!/^\d+$/.test(index)) return c.json({ error: "Invalid index" }, 400);

	const thread = getThreadFromDoc(dataSpace, id);
	if (!thread) return c.json({ error: "Thread not found" }, 404);

	if (!thread.tweetImages?.[index]) return c.json({ ok: true });

	await deleteImageFile(thread.tweetImages[index]);

	const docId = socialsDocId(dataSpace);
	_syncServer!.changeDoc<SocialsDoc>(docId, "remove tweet image", (d) => {
		if (d.threads?.[id]?.tweetImages?.[index]) {
			delete d.threads[id].tweetImages![index];
			if (Object.keys(d.threads[id].tweetImages || {}).length === 0) {
				delete d.threads[id].tweetImages;
			}
			d.threads[id].updatedAt = Date.now();
		}
	});

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

routes.delete("/api/threads/:id/images", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c.get("effectiveSpace" as any) as string) || space;
	const id = c.req.param("id");
	if (!id || id.includes("..") || id.includes("/")) return c.json({ error: "Invalid ID" }, 400);

	const thread = getThreadFromDoc(dataSpace, id);
	if (!thread) return c.json({ ok: true }); // Thread already gone

	// Clean up header image
	if (thread.imageUrl) await deleteImageFile(thread.imageUrl);

	// Clean up per-tweet images
	if (thread.tweetImages) {
		for (const url of Object.values(thread.tweetImages)) {
			await deleteImageFile(url);
		}
	}

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

// ── Newsletter (Listmonk) proxy API ──

async function requireNewsletterRole(c: any, minRole: SpaceRoleString): Promise<{ claims: EncryptIDClaims; role: SpaceRoleString } | Response> {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	let claims: EncryptIDClaims;
	try { claims = await verifyEncryptIDToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const space = c.req.param("space") || "demo";
	const result = await resolveCallerRole(space, claims);
	if (!result || !roleAtLeast(result.role, minRole)) {
		return c.json({ error: "Insufficient permissions" }, 403);
	}
	return { claims, role: result.role };
}

routes.get("/api/newsletter/status", async (c) => {
	const space = c.req.param("space") || "demo";
	const config = await getListmonkConfig(space);
	return c.json({ configured: !!config });
});

routes.get("/api/newsletter/lists", async (c) => {
	const auth = await requireNewsletterRole(c, "moderator");
	if (auth instanceof Response) return auth;

	const space = c.req.param("space") || "demo";
	const config = await getListmonkConfig(space);
	if (!config) return c.json({ error: "Listmonk not configured" }, 404);

	const res = await listmonkFetch(config, "/api/lists");
	const data = await res.json();
	return c.json(data, res.status as any);
});

routes.get("/api/newsletter/subscribers", async (c) => {
	const auth = await requireNewsletterRole(c, "moderator");
	if (auth instanceof Response) return auth;

	const space = c.req.param("space") || "demo";
	const config = await getListmonkConfig(space);
	if (!config) return c.json({ error: "Listmonk not configured" }, 404);

	const url = new URL(c.req.url);
	const query = url.search || "";
	const res = await listmonkFetch(config, `/api/subscribers${query}`);
	const data = await res.json();
	return c.json(data, res.status as any);
});

routes.get("/api/newsletter/campaigns", async (c) => {
	const auth = await requireNewsletterRole(c, "moderator");
	if (auth instanceof Response) return auth;

	const space = c.req.param("space") || "demo";
	const config = await getListmonkConfig(space);
	if (!config) return c.json({ error: "Listmonk not configured" }, 404);

	const res = await listmonkFetch(config, "/api/campaigns");
	const data = await res.json();
	return c.json(data, res.status as any);
});

routes.post("/api/newsletter/campaigns", async (c) => {
	const auth = await requireNewsletterRole(c, "admin");
	if (auth instanceof Response) return auth;

	const space = c.req.param("space") || "demo";
	const config = await getListmonkConfig(space);
	if (!config) return c.json({ error: "Listmonk not configured" }, 404);

	const body = await c.req.text();
	const res = await listmonkFetch(config, "/api/campaigns", { method: "POST", body });
	const data = await res.json();
	return c.json(data, res.status as any);
});

routes.put("/api/newsletter/campaigns/:id/status", async (c) => {
	const auth = await requireNewsletterRole(c, "moderator");
	if (auth instanceof Response) return auth;

	const space = c.req.param("space") || "demo";
	const config = await getListmonkConfig(space);
	if (!config) return c.json({ error: "Listmonk not configured" }, 404);

	const campaignId = c.req.param("id");
	const body = await c.req.text();
	const res = await listmonkFetch(config, `/api/campaigns/${campaignId}/status`, { method: "PUT", body });
	const data = await res.json();
	return c.json(data, res.status as any);
});

// ── Page routes (inject web components) ──

routes.get("/campaign", (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c.get("effectiveSpace" as any) as string) || space;
	return c.html(renderShell({
		title: `Campaign — rSocials | rSpace`,
		moduleId: "rsocials",
		spaceSlug: space,
		modules: getModuleInfoList(),
		theme: "dark",
		body: `<folk-campaign-manager space="${escapeHtml(space)}"></folk-campaign-manager>`,
		styles: `<link rel="stylesheet" href="/modules/rsocials/socials.css">`,
		scripts: `<script type="module" src="/modules/rsocials/folk-campaign-manager.js"></script>`,
	}));
});

routes.get("/thread/:id", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c.get("effectiveSpace" as any) as string) || space;
	const id = c.req.param("id");
	if (!id || id.includes("..") || id.includes("/")) return c.text("Not found", 404);

	const thread = getThreadFromDoc(dataSpace, id);
	if (!thread) return c.text("Thread not found", 404);

	// OG tags for social crawlers (SSR)
	const desc = escapeHtml((thread.tweets[0] || "").substring(0, 200));
	const titleText = escapeHtml(`Thread by ${thread.handle}`);
	const origin = "https://rspace.online";

	let ogHead = `
		<meta property="og:title" content="${titleText}">
		<meta property="og:description" content="${desc}">
		<meta property="og:type" content="article">
		<meta name="twitter:card" content="summary_large_image">
		<meta name="twitter:title" content="${titleText}">
		<meta name="twitter:description" content="${desc}">`;
	if (thread.imageUrl) {
		ogHead += `
		<meta property="og:image" content="${origin}${thread.imageUrl}">
		<meta name="twitter:image" content="${origin}${thread.imageUrl}">`;
	}

	// Hydrate thread data for the component
	const dataScript = `<script>window.__THREAD_DATA__ = ${JSON.stringify(thread).replace(/</g, "\\u003c")};</script>`;

	return c.html(renderShell({
		title: `${thread.title || "Thread"} — rSocials | rSpace`,
		moduleId: "rsocials",
		spaceSlug: space,
		modules: getModuleInfoList(),
		theme: "dark",
		body: `${dataScript}<folk-thread-builder space="${escapeHtml(space)}" thread-id="${escapeHtml(id)}" mode="readonly"></folk-thread-builder>`,
		styles: `<link rel="stylesheet" href="/modules/rsocials/socials.css">`,
		scripts: `<script type="module" src="/modules/rsocials/folk-thread-builder.js"></script>`,
		head: ogHead,
	}));
});

routes.get("/thread-editor/:id/edit", async (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c.get("effectiveSpace" as any) as string) || space;
	const id = c.req.param("id");
	if (!id || id.includes("..") || id.includes("/")) return c.text("Not found", 404);

	const thread = getThreadFromDoc(dataSpace, id);
	if (!thread) return c.text("Thread not found", 404);

	const dataScript = `<script>window.__THREAD_DATA__ = ${JSON.stringify(thread).replace(/</g, "\\u003c")};</script>`;

	return c.html(renderShell({
		title: `Edit: ${thread.title || "Thread"} — rSocials | rSpace`,
		moduleId: "rsocials",
		spaceSlug: space,
		modules: getModuleInfoList(),
		theme: "dark",
		body: `${dataScript}<folk-thread-builder space="${escapeHtml(space)}" thread-id="${escapeHtml(id)}" mode="edit"></folk-thread-builder>`,
		styles: `<link rel="stylesheet" href="/modules/rsocials/socials.css">`,
		scripts: `<script type="module" src="/modules/rsocials/folk-thread-builder.js"></script>`,
	}));
});

routes.get("/thread-editor", (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c.get("effectiveSpace" as any) as string) || space;
	return c.html(renderShell({
		title: `Thread Editor — rSocials | rSpace`,
		moduleId: "rsocials",
		spaceSlug: space,
		modules: getModuleInfoList(),
		theme: "dark",
		body: `<folk-thread-builder space="${escapeHtml(space)}" mode="new"></folk-thread-builder>`,
		styles: `<link rel="stylesheet" href="/modules/rsocials/socials.css">`,
		scripts: `<script type="module" src="/modules/rsocials/folk-thread-builder.js"></script>`,
	}));
});

routes.get("/threads", (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c.get("effectiveSpace" as any) as string) || space;
	return c.html(renderShell({
		title: `Threads — rSocials | rSpace`,
		moduleId: "rsocials",
		spaceSlug: space,
		modules: getModuleInfoList(),
		theme: "dark",
		body: `<folk-thread-gallery space="${escapeHtml(space)}"></folk-thread-gallery>`,
		styles: `<link rel="stylesheet" href="/modules/rsocials/socials.css">`,
		scripts: `<script type="module" src="/modules/rsocials/folk-thread-gallery.js"></script>`,
	}));
});

routes.get("/campaigns", (c) => {
	const space = c.req.param("space") || "demo";
	return c.html(renderShell({
		title: `Campaign Planner — rSocials | rSpace`,
		moduleId: "rsocials",
		spaceSlug: space,
		modules: getModuleInfoList(),
		theme: "dark",
		body: `<folk-campaign-planner space="${escapeHtml(space)}"></folk-campaign-planner>`,
		styles: `<link rel="stylesheet" href="/modules/rsocials/socials.css"><link rel="stylesheet" href="/modules/rsocials/campaign-planner.css">`,
		scripts: `<script type="module" src="/modules/rsocials/folk-campaign-planner.js"></script>`,
	}));
});

// ── Demo feed rendering (server-rendered, no web component needed) ──

function renderDemoFeedHTML(): string {
	const cards = DEMO_FEED.map(
		(post) => `
		<article class="rsocials-item">
			<div class="rsocials-item-header">
				<div class="rsocials-avatar" style="background:${post.color}">${post.initial}</div>
				<div class="rsocials-meta">
					<strong>${post.username}</strong>
					<time>${post.timeAgo}</time>
				</div>
			</div>
			<p class="rsocials-item-content">${post.content}</p>
			<div class="rsocials-item-actions">
				<span class="rsocials-action"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg> ${post.likes}</span>
				<span class="rsocials-action"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg> ${post.replies}</span>
			</div>
		</article>`,
	).join("\n");

	return `
		<div class="rsocials-app rsocials-demo">
			<div class="rsocials-header">
				<div style="display:flex;align-items:center;justify-content:space-between;gap:1rem">
					<h2>Social Feed <span class="rsocials-demo-badge">DEMO</span></h2>
					<a href="?view=app" class="rapp-nav__btn--app-toggle">Open Full App</a>
				</div>
				<p class="rsocials-subtitle">A preview of your community's social timeline</p>
			</div>
			<div class="rsocials-feed">${cards}</div>
			<p class="rsocials-demo-notice">This is demo data. Connect ActivityPub or RSS feeds in your own space.</p>
		</div>`;
}

// ── Path-based sub-routes ──

// ── Postiz scheduler — embedded via iframe ──
// Postiz runs at demo.rsocials.online (Traefik priority 130 > rSpace's 120).
// The /scheduler route renders a full-page iframe shell.
const POSTIZ_URL = process.env.POSTIZ_URL || "https://demo.rsocials.online";


routes.get("/scheduler", (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c.get("effectiveSpace" as any) as string) || space;
	return c.html(renderExternalAppShell({
		title: `Post Scheduler — rSocials | rSpace`,
		moduleId: "rsocials",
		spaceSlug: space,
		modules: getModuleInfoList(),
		theme: "dark",
		appName: "Postiz",
		appUrl: POSTIZ_URL,
	}));
});

routes.get("/newsletter-list", async (c) => {
	const space = c.req.param("space") || "demo";

	// Resolve caller role for UI gating (viewer fallback for unauthenticated)
	let role: SpaceRoleString = 'viewer';
	const token = extractToken(c.req.raw.headers);
	if (token) {
		try {
			const claims = await verifyEncryptIDToken(token);
			const result = await resolveCallerRole(space, claims);
			if (result) role = result.role;
		} catch { /* keep viewer default */ }
	}

	return c.html(renderShell({
		title: `Newsletter — rSocials | rSpace`,
		moduleId: "rsocials",
		spaceSlug: space,
		modules: getModuleInfoList(),
		theme: "dark",
		body: `<folk-newsletter-manager space="${escapeHtml(space)}" role="${escapeHtml(role)}"></folk-newsletter-manager>`,
		styles: `<link rel="stylesheet" href="/modules/rsocials/newsletter.css">`,
		scripts: `<script type="module" src="/modules/rsocials/folk-newsletter-manager.js"></script>`,
	}));
});

routes.get("/feed", (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c.get("effectiveSpace" as any) as string) || space;
	const isDemo = space === "demo";
	const body = isDemo ? renderDemoFeedHTML() : renderLanding();
	const styles = isDemo
		? `<link rel="stylesheet" href="/modules/rsocials/socials.css">`
		: `<style>${RICH_LANDING_CSS}</style>`;
	return c.html(renderShell({
		title: `${space} — Socials Feed | rSpace`,
		moduleId: "rsocials",
		spaceSlug: space,
		modules: getModuleInfoList(),
		theme: "dark",
		body,
		styles,
	}));
});

routes.get("/landing", (c) => {
	const space = c.req.param("space") || "demo";
	const dataSpace = (c.get("effectiveSpace" as any) as string) || space;
	return c.html(renderShell({
		title: `${space} — rSocials | rSpace`,
		moduleId: "rsocials",
		spaceSlug: space,
		modules: getModuleInfoList(),
		theme: "dark",
		body: renderLanding(),
		styles: `<style>${RICH_LANDING_CSS}</style>`,
	}));
});

// ── Default: rSocials hub with navigation ──

routes.get("/", (c) => {
	const space = c.req.param("space") || "demo";
	const host = c.req.header("host")?.split(":")[0] || "";
	const isSubdomain = (host.endsWith(".rspace.online") && host !== "rspace.online" && !host.startsWith("www.")) || host.endsWith(".rsocials.online");
	const base = isSubdomain ? "/rsocials" : `/${escapeHtml(space)}/rsocials`;
	return c.html(renderShell({
		title: `rSocials — ${space} | rSpace`,
		moduleId: "rsocials",
		spaceSlug: space,
		modules: getModuleInfoList(),
		theme: "dark",
		styles: `<style>
.rs-hub{max-width:720px;margin:3rem auto;padding:0 1.5rem}
.rs-hub h1{font-size:1.8rem;margin-bottom:.5rem}
.rs-hub p{color:var(--rs-text-secondary,#aaa);margin-bottom:2rem}
.rs-nav{display:flex;flex-direction:column;gap:1rem}
.rs-nav a{display:flex;align-items:center;gap:1rem;padding:1.25rem 1.5rem;border-radius:12px;background:var(--rs-surface,#1e1e2e);border:1px solid var(--rs-border,#333);text-decoration:none;color:inherit;transition:border-color .15s,background .15s}
.rs-nav a:hover{border-color:var(--rs-accent,#14b8a6);background:var(--rs-surface-hover,#252538)}
.rs-nav .nav-icon{font-size:2rem;flex-shrink:0}
.rs-nav .nav-body h3{margin:0 0 .25rem;font-size:1.1rem}
.rs-nav .nav-body p{margin:0;font-size:.85rem;color:var(--rs-text-secondary,#aaa)}
</style>`,
		body: `<div class="rs-hub">
  <h1>rSocials</h1>
  <p>Social media tools for your community</p>
  <nav class="rs-nav">
    <a href="${base}/campaigns">
      <span class="nav-icon">📢</span>
      <div class="nav-body">
        <h3>Campaigns</h3>
        <p>Plan and manage multi-platform social media campaigns</p>
      </div>
    </a>
    <a href="${base}/threads">
      <span class="nav-icon">📋</span>
      <div class="nav-body">
        <h3>Threads</h3>
        <p>Browse saved thread drafts and published threads</p>
      </div>
    </a>
    <a href="${base}/thread-editor">
      <span class="nav-icon">🧵</span>
      <div class="nav-body">
        <h3>Thread Editor</h3>
        <p>Compose and preview tweet threads with live card preview</p>
      </div>
    </a>
    <a href="${base}/newsletter-list">
      <span class="nav-icon">📧</span>
      <div class="nav-body">
        <h3>Newsletter List</h3>
        <p>Manage newsletter subscribers and send campaigns via Listmonk</p>
      </div>
    </a>
  </nav>
</div>`,
	}));
});

// ── Module export ──

export const socialsModule: RSpaceModule = {
	id: "rsocials",
	name: "rSocials",
	icon: "📢",
	description: "Federated social feed aggregator for communities",
	scoping: { defaultScope: "space", userConfigurable: true },
	docSchemas: [{ pattern: "{space}:socials:data", description: "Threads and campaigns", init: socialsSchema.init }],
	routes,
	publicWrite: true,
	settingsSchema: [
		{ key: 'listmonkUrl', label: 'Listmonk URL', type: 'string', description: 'Base URL of your Listmonk instance (e.g. https://newsletter.example.com)' },
		{ key: 'listmonkUser', label: 'Listmonk Username', type: 'string', description: 'API username for Listmonk' },
		{ key: 'listmonkPassword', label: 'Listmonk Password', type: 'password', description: 'API password for Listmonk' },
	],
	standaloneDomain: "rsocials.online",
	landingPage: renderLanding,
	seedTemplate: seedTemplateSocials,
	async onInit(ctx) {
		_syncServer = ctx.syncServer;
		// Run migration for any existing file-based threads
		try { await migrateFileThreadsToAutomerge("demo"); } catch { /* ignore */ }
	},
	externalApp: { url: POSTIZ_URL, name: "Postiz" },
	feeds: [
		{
			id: "social-feed",
			name: "Social Feed",
			kind: "data",
			description: "Community social timeline — posts, links, and activity from connected platforms",
		},
	],
	acceptsFeeds: ["data", "trust"],
	outputPaths: [
		{ path: "campaigns", name: "Campaigns", icon: "📢", description: "Social media campaigns" },
		{ path: "posts", name: "Posts", icon: "📱", description: "Social feed posts across platforms" },
	],
	subPageInfos: [
		{
			path: "campaigns",
			title: "Campaign Manager",
			icon: "📢",
			tagline: "rSocials Tool",
			description: "Plan and track multi-platform social media campaigns with scheduling, analytics, and team collaboration.",
			features: [
				{ icon: "📅", title: "Schedule Posts", text: "Queue posts across platforms with a visual calendar timeline." },
				{ icon: "📊", title: "Track Performance", text: "Monitor engagement metrics and campaign reach in one dashboard." },
				{ icon: "👥", title: "Team Workflow", text: "Draft, review, and approve posts collaboratively before publishing." },
			],
		},
		{
			path: "threads",
			title: "Thread Gallery",
			icon: "📋",
			tagline: "rSocials Tool",
			description: "Browse all saved thread drafts in your community. Find inspiration, remix threads, or pick up where you left off.",
		},
		{
			path: "thread-editor",
			title: "Thread Editor",
			icon: "🧵",
			tagline: "rSocials Tool",
			description: "Compose, preview, and schedule tweet threads with a live card-by-card preview. Save drafts, generate share images, and publish when ready.",
			features: [
				{ icon: "✍️", title: "Live Preview", text: "See your thread as tweet cards in real time as you type, with character counts and thread numbering." },
				{ icon: "💾", title: "Save & Edit Drafts", text: "Save thread drafts to your space, revisit and refine them before publishing." },
				{ icon: "🖼️", title: "Share Images", text: "Auto-generate a branded share image of your thread for cross-posting." },
			],
		},
		{
			path: "newsletter-list",
			title: "Newsletter List",
			icon: "📧",
			tagline: "rSocials Tool",
			description: "Manage newsletter subscribers and send email campaigns via the embedded Listmonk interface.",
			features: [
				{ icon: "👥", title: "Subscriber Management", text: "View, import, and manage newsletter subscribers across multiple mailing lists." },
				{ icon: "📨", title: "Email Campaigns", text: "Compose and send email campaigns with templates and scheduling." },
				{ icon: "📊", title: "Analytics", text: "Track open rates, click-throughs, and subscriber engagement." },
			],
		},
	],
};
