/**
 * Vote module — conviction voting engine.
 *
 * Credit-weighted conviction voting for collaborative governance.
 * Spaces run ranked proposals with configurable parameters.
 *
 * All state stored in Automerge documents via SyncServer.
 * Doc layout:
 *   {space}:vote:config                     → SpaceConfig (stored on a ProposalDoc)
 *   {space}:vote:proposals:{proposalId}     → ProposalDoc with votes/finalVotes
 */

import { Hono } from "hono";
import * as Automerge from '@automerge/automerge';
import { renderShell } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
import { verifyToken, extractToken } from "../../server/auth";
import { renderLanding } from "./landing";
import type { SyncServer } from '../../server/local-first/sync-server';
import { proposalSchema, proposalDocId, computeElo, ELO_DEFAULT } from './schemas';
import type { ProposalDoc, SpaceConfig } from './schemas';

const routes = new Hono();

// ── SyncServer ref (set during onInit) ──
let _syncServer: SyncServer | null = null;

// ── DocId helpers ──

function spaceConfigDocId(space: string) {
	return `${space}:vote:config` as const;
}

// ── Automerge helpers ──

/** Ensure a proposal doc exists, creating it if needed. */
function ensureProposalDoc(space: string, proposalId: string): ProposalDoc {
	const docId = proposalDocId(space, proposalId);
	let doc = _syncServer!.getDoc<ProposalDoc>(docId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<ProposalDoc>(), 'init proposal', (d) => {
			const init = proposalSchema.init();
			Object.assign(d, init);
			d.meta.spaceSlug = space;
			d.proposal.id = proposalId;
			d.proposal.spaceSlug = space;
		});
		_syncServer!.setDoc(docId, doc);
	}
	return doc;
}

/** Ensure a space config doc exists, creating it if needed. Returns the doc. */
function ensureSpaceConfigDoc(space: string): ProposalDoc {
	const docId = spaceConfigDocId(space);
	let doc = _syncServer!.getDoc<ProposalDoc>(docId);
	if (!doc) {
		doc = Automerge.change(Automerge.init<ProposalDoc>(), 'init space config', (d) => {
			const init = proposalSchema.init();
			Object.assign(d, init);
			d.meta.spaceSlug = space;
			d.spaceConfig = {
				slug: space,
				name: '',
				description: '',
				ownerDid: '',
				visibility: 'public',
				promotionThreshold: 100,
				votingPeriodDays: 7,
				creditsPerDay: 10,
				maxCredits: 500,
				startingCredits: 50,
				createdAt: Date.now(),
				updatedAt: Date.now(),
			};
		});
		_syncServer!.setDoc(docId, doc);
	}
	return doc;
}

/** Get all space config docs across all spaces. */
function listSpaceConfigDocs(): { docId: string; doc: ProposalDoc }[] {
	if (!_syncServer) return [];
	const results: { docId: string; doc: ProposalDoc }[] = [];
	for (const docId of _syncServer.listDocs()) {
		if (docId.endsWith(':vote:config')) {
			const doc = _syncServer.getDoc<ProposalDoc>(docId);
			if (doc?.spaceConfig) results.push({ docId, doc });
		}
	}
	return results;
}

/** Get all proposal docs for a space. */
function listProposalDocs(space: string): { docId: string; doc: ProposalDoc }[] {
	if (!_syncServer) return [];
	const results: { docId: string; doc: ProposalDoc }[] = [];
	const prefix = `${space}:vote:proposals:`;
	for (const docId of _syncServer.listDocs()) {
		if (docId.startsWith(prefix)) {
			const doc = _syncServer.getDoc<ProposalDoc>(docId);
			if (doc) results.push({ docId, doc });
		}
	}
	return results;
}

/** Get all proposal docs across all spaces. */
function listAllProposalDocs(): { docId: string; doc: ProposalDoc }[] {
	if (!_syncServer) return [];
	const results: { docId: string; doc: ProposalDoc }[] = [];
	for (const docId of _syncServer.listDocs()) {
		if (docId.includes(':vote:proposals:')) {
			const doc = _syncServer.getDoc<ProposalDoc>(docId);
			if (doc) results.push({ docId, doc });
		}
	}
	return results;
}

// ── Conversion helpers (Automerge → REST format) ──

function spaceConfigToRest(cfg: SpaceConfig) {
	return {
		slug: cfg.slug,
		name: cfg.name,
		description: cfg.description,
		owner_did: cfg.ownerDid,
		visibility: cfg.visibility,
		promotion_threshold: cfg.promotionThreshold,
		voting_period_days: cfg.votingPeriodDays,
		credits_per_day: cfg.creditsPerDay,
		max_credits: cfg.maxCredits,
		starting_credits: cfg.startingCredits,
		created_at: new Date(cfg.createdAt).toISOString(),
		updated_at: new Date(cfg.updatedAt).toISOString(),
	};
}

function proposalToRest(doc: ProposalDoc) {
	const p = doc.proposal;
	const voteCount = Object.keys(doc.votes).length;
	const pw = doc.pairwise || { elo: ELO_DEFAULT, comparisons: 0, wins: 0 };
	return {
		id: p.id,
		space_slug: p.spaceSlug,
		author_id: p.authorId,
		title: p.title,
		description: p.description,
		status: p.status,
		score: p.score,
		voting_ends_at: p.votingEndsAt ? new Date(p.votingEndsAt).toISOString() : null,
		final_yes: p.finalYes,
		final_no: p.finalNo,
		final_abstain: p.finalAbstain,
		vote_count: String(voteCount),
		elo: pw.elo,
		elo_comparisons: pw.comparisons,
		elo_wins: pw.wins,
		created_at: new Date(p.createdAt).toISOString(),
		updated_at: new Date(p.updatedAt).toISOString(),
	};
}

// ── Helper: calculate effective weight with decay ──
function getEffectiveWeight(weight: number, createdAt: number): number {
	const ageMs = Date.now() - createdAt;
	const ageDays = ageMs / (1000 * 60 * 60 * 24);
	if (ageDays < 30) return weight;
	if (ageDays >= 60) return 0;
	const decayProgress = (ageDays - 30) / 30;
	return Math.round(weight * (1 - decayProgress));
}

// ── Helper: recalculate proposal score from votes ──
function recalcScore(doc: ProposalDoc): number {
	let score = 0;
	for (const v of Object.values(doc.votes)) {
		score += getEffectiveWeight(v.weight, v.createdAt);
	}
	return score;
}

// ── Helper: generate unique ID ──
function newId(): string {
	return crypto.randomUUID();
}

// ── Seed demo data into Automerge ──
function seedDemoIfEmpty(space: string = 'community') {
	if (!_syncServer) return;
	// If this space already has proposals, skip (or was already seeded)
	const _seedConfigDoc = _syncServer!.getDoc<ProposalDoc>(spaceConfigDocId(space));
	if ((_seedConfigDoc?.meta as any)?.seeded || listProposalDocs(space).length > 0) return;

	// Ensure space config exists
	ensureSpaceConfigDoc(space);
	_syncServer!.changeDoc<ProposalDoc>(spaceConfigDocId(space), 'seed space config', (d) => {
		if (d.spaceConfig!.name) return; // already configured
		d.spaceConfig!.name = 'Community Governance';
		d.spaceConfig!.description = 'Proposals for the rSpace ecosystem';
		d.spaceConfig!.ownerDid = 'did:demo:seed';
		d.spaceConfig!.promotionThreshold = 100;
		d.spaceConfig!.votingPeriodDays = 7;
		d.spaceConfig!.creditsPerDay = 10;
		d.spaceConfig!.maxCredits = 500;
		d.spaceConfig!.startingCredits = 50;
	});

	const demoUserId = 'did:demo:seed';
	const now = Date.now();

	const proposals = [
		{ title: "Add dark mode across all r* modules", desc: "Implement a consistent dark theme with a toggle in shell.css. Use CSS custom properties for theming so each module inherits automatically.", status: "RANKING", score: 45 },
		{ title: "Implement real-time collaboration in rNotes", desc: "Use Automerge CRDTs (already in the stack) to enable simultaneous editing of notes, similar to how rSpace canvas works.", status: "RANKING", score: 72 },
		{ title: "Adopt cosmolocal print-on-demand for all merch", desc: "Route all merchandise orders through the provider registry to find the closest printer. Reduces shipping emissions and supports local economies.", status: "VOTING", score: 105 },
		{ title: "Use EncryptID passkeys for all authentication", desc: "Standardize on WebAuthn passkeys via EncryptID across the entire r* ecosystem. One passkey, all apps.", status: "PASSED", score: 150 },
		{ title: "Switch from PostgreSQL to SQLite for simpler deployment", desc: "Evaluate replacing PostgreSQL with SQLite for modules that don't need concurrent writes.", status: "FAILED", score: 30 },
	];

	for (const p of proposals) {
		const pid = newId();
		const docId = proposalDocId(space, pid);

		let doc = Automerge.change(Automerge.init<ProposalDoc>(), 'seed proposal', (d) => {
			const init = proposalSchema.init();
			Object.assign(d, init);
			d.meta.spaceSlug = space;
			d.proposal.id = pid;
			d.proposal.spaceSlug = space;
			d.proposal.authorId = demoUserId;
			d.proposal.title = p.title;
			d.proposal.description = p.desc;
			d.proposal.status = p.status;
			d.proposal.score = p.score;
			d.proposal.createdAt = now;
			d.proposal.updatedAt = now;
		});

		if (p.status === "VOTING") {
			doc = Automerge.change(doc, 'set voting tally', (d) => {
				d.proposal.votingEndsAt = now + 5 * 24 * 60 * 60 * 1000;
				d.proposal.finalYes = 5;
				d.proposal.finalNo = 2;
			});
		} else if (p.status === "PASSED") {
			doc = Automerge.change(doc, 'set passed tally', (d) => {
				d.proposal.finalYes = 12;
				d.proposal.finalNo = 3;
				d.proposal.finalAbstain = 2;
			});
		} else if (p.status === "FAILED") {
			doc = Automerge.change(doc, 'set failed tally', (d) => {
				d.proposal.finalYes = 2;
				d.proposal.finalNo = 8;
				d.proposal.finalAbstain = 1;
			});
		}

		_syncServer!.setDoc(docId, doc);
	}

	_syncServer!.changeDoc<ProposalDoc>(spaceConfigDocId(space), 'mark seeded', (d) => {
		if (d.meta) (d.meta as any).seeded = true;
	});
	console.log(`[Vote] Demo data seeded for "${space}": 1 space config, 5 proposals`);
}

// ── Spaces API ──

// GET /api/spaces — list spaces
routes.get("/api/spaces", (c) => {
	const spaceDocs = listSpaceConfigDocs();
	const spaces = spaceDocs
		.filter((s) => s.doc.spaceConfig !== null)
		.map((s) => spaceConfigToRest(s.doc.spaceConfig!))
		.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
		.slice(0, 50);
	return c.json({ spaces });
});

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

	const body = await c.req.json();
	const { name, slug, description, visibility = "public" } = body;
	if (!name || !slug) return c.json({ error: "name and slug required" }, 400);
	if (!/^[a-z0-9-]+$/.test(slug)) return c.json({ error: "Invalid slug" }, 400);

	// Check if space already exists
	const existing = _syncServer!.getDoc<ProposalDoc>(spaceConfigDocId(slug));
	if (existing?.spaceConfig?.name) return c.json({ error: "Space already exists" }, 409);

	const now = Date.now();
	const doc = ensureSpaceConfigDoc(slug);
	_syncServer!.changeDoc<ProposalDoc>(spaceConfigDocId(slug), 'create space', (d) => {
		d.spaceConfig!.slug = slug;
		d.spaceConfig!.name = name;
		d.spaceConfig!.description = description || '';
		d.spaceConfig!.ownerDid = claims.sub;
		d.spaceConfig!.visibility = visibility;
		d.spaceConfig!.createdAt = now;
		d.spaceConfig!.updatedAt = now;
	});

	const updated = _syncServer!.getDoc<ProposalDoc>(spaceConfigDocId(slug));
	return c.json(spaceConfigToRest(updated!.spaceConfig!), 201);
});

// GET /api/spaces/:slug — space detail
routes.get("/api/spaces/:slug", (c) => {
	const slug = c.req.param("slug");
	const doc = _syncServer!.getDoc<ProposalDoc>(spaceConfigDocId(slug));
	if (!doc?.spaceConfig?.name) return c.json({ error: "Space not found" }, 404);
	return c.json(spaceConfigToRest(doc.spaceConfig));
});

// ── Proposals API ──

// GET /api/proposals — list proposals (query: space_slug, status)
routes.get("/api/proposals", (c) => {
	const { space_slug, status, limit = "50", offset = "0" } = c.req.query();
	const maxLimit = Math.min(parseInt(limit) || 50, 100);
	const startOffset = parseInt(offset) || 0;

	let docs: { docId: string; doc: ProposalDoc }[];
	if (space_slug) {
		docs = listProposalDocs(space_slug);
	} else {
		docs = listAllProposalDocs();
	}

	let proposals = docs
		.filter((d) => d.doc.proposal.title) // exclude empty/config docs
		.map((d) => proposalToRest(d.doc));

	if (status) {
		proposals = proposals.filter((p) => p.status === status);
	}

	// Sort by score descending, then created_at descending
	proposals.sort((a, b) => {
		if (b.score !== a.score) return b.score - a.score;
		return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
	});

	proposals = proposals.slice(startOffset, startOffset + maxLimit);
	return c.json({ proposals });
});

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

	const body = await c.req.json();
	const { space_slug, title, description } = body;
	if (!space_slug || !title) return c.json({ error: "space_slug and title required" }, 400);

	// Verify space exists
	const spaceDoc = _syncServer!.getDoc<ProposalDoc>(spaceConfigDocId(space_slug));
	if (!spaceDoc?.spaceConfig?.name) return c.json({ error: "Space not found" }, 404);

	const pid = newId();
	const now = Date.now();
	const docId = proposalDocId(space_slug, pid);

	const doc = Automerge.change(Automerge.init<ProposalDoc>(), 'create proposal', (d) => {
		const init = proposalSchema.init();
		Object.assign(d, init);
		d.meta.spaceSlug = space_slug;
		d.proposal.id = pid;
		d.proposal.spaceSlug = space_slug;
		d.proposal.authorId = claims.sub;
		d.proposal.title = title;
		d.proposal.description = description || '';
		d.proposal.createdAt = now;
		d.proposal.updatedAt = now;
	});
	_syncServer!.setDoc(docId, doc);

	// Notify space members about the new proposal
	import('../rinbox/agent-notify').then(({ sendSpaceNotification }) => {
		sendSpaceNotification(space_slug, `New Proposal: ${title}`,
			`<h3>${title}</h3>${description ? `<p>${description}</p>` : ''}<p><a href="https://${space_slug}.rspace.online/rvote">Vote in rVote</a></p>`
		).catch(() => {});
	}).catch(() => {});

	return c.json(proposalToRest(doc), 201);
});

// GET /api/proposals/:id — proposal detail
routes.get("/api/proposals/:id", (c) => {
	const id = c.req.param("id");

	// Search across all spaces for this proposal
	const allDocs = listAllProposalDocs();
	const match = allDocs.find((d) => d.doc.proposal.id === id);
	if (!match) return c.json({ error: "Proposal not found" }, 404);

	return c.json(proposalToRest(match.doc));
});

// POST /api/proposals/:id/vote — cast conviction vote
routes.post("/api/proposals/:id/vote", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	let claims;
	try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const id = c.req.param("id");
	const body = await c.req.json();
	const { weight = 1 } = body;

	// Find proposal
	const allDocs = listAllProposalDocs();
	const match = allDocs.find((d) => d.doc.proposal.id === id);
	if (!match) return c.json({ error: "Proposal not found" }, 404);
	if (match.doc.proposal.status !== "RANKING") return c.json({ error: "Proposal not in ranking phase" }, 400);

	const userId = claims.sub;
	const creditCost = weight * weight; // quadratic cost
	const now = Date.now();
	const thirtyDaysMs = 30 * 24 * 60 * 60 * 1000;

	// Upsert vote (keyed by userId)
	_syncServer!.changeDoc<ProposalDoc>(match.docId, 'cast conviction vote', (d) => {
		d.votes[userId] = {
			id: d.votes[userId]?.id || newId(),
			userId,
			proposalId: id,
			weight,
			creditCost,
			createdAt: now,
			decaysAt: now + thirtyDaysMs,
		};
	});

	// Re-read doc, recalculate score
	const updatedDoc = _syncServer!.getDoc<ProposalDoc>(match.docId)!;
	const score = recalcScore(updatedDoc);

	// Update score on the doc
	_syncServer!.changeDoc<ProposalDoc>(match.docId, 'update score', (d) => {
		d.proposal.score = score;
		d.proposal.updatedAt = Date.now();
	});

	// Check for promotion to VOTING phase
	const spaceSlug = updatedDoc.proposal.spaceSlug;
	const spaceDoc = _syncServer!.getDoc<ProposalDoc>(spaceConfigDocId(spaceSlug));
	const threshold = spaceDoc?.spaceConfig?.promotionThreshold || 100;

	if (score >= threshold && updatedDoc.proposal.status === "RANKING") {
		const votingDays = spaceDoc?.spaceConfig?.votingPeriodDays || 7;
		const votingEndsAt = Date.now() + votingDays * 24 * 60 * 60 * 1000;
		_syncServer!.changeDoc<ProposalDoc>(match.docId, 'promote to voting', (d) => {
			d.proposal.status = 'VOTING';
			d.proposal.votingEndsAt = votingEndsAt;
			d.proposal.updatedAt = Date.now();
		});
	}

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

// POST /api/proposals/:id/final-vote — cast binary vote
routes.post("/api/proposals/:id/final-vote", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	let claims;
	try { claims = await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const id = c.req.param("id");
	const body = await c.req.json();
	const { vote } = body;
	if (!["YES", "NO", "ABSTAIN"].includes(vote)) return c.json({ error: "Invalid vote" }, 400);

	// Find proposal
	const allDocs = listAllProposalDocs();
	const match = allDocs.find((d) => d.doc.proposal.id === id);
	if (!match) return c.json({ error: "Proposal not found" }, 404);
	if (match.doc.proposal.status !== "VOTING") return c.json({ error: "Proposal not in voting phase" }, 400);

	const userId = claims.sub;
	const now = Date.now();

	// Upsert final vote (keyed by userId)
	_syncServer!.changeDoc<ProposalDoc>(match.docId, 'cast final vote', (d) => {
		d.finalVotes[userId] = {
			id: d.finalVotes[userId]?.id || newId(),
			userId,
			proposalId: id,
			vote: vote as 'YES' | 'NO' | 'ABSTAIN',
			createdAt: now,
		};
	});

	// Tally final votes
	const updatedDoc = _syncServer!.getDoc<ProposalDoc>(match.docId)!;
	const tally: Record<string, number> = { YES: 0, NO: 0, ABSTAIN: 0 };
	for (const fv of Object.values(updatedDoc.finalVotes)) {
		tally[fv.vote] = (tally[fv.vote] || 0) + 1;
	}

	_syncServer!.changeDoc<ProposalDoc>(match.docId, 'update final tally', (d) => {
		d.proposal.finalYes = tally.YES;
		d.proposal.finalNo = tally.NO;
		d.proposal.finalAbstain = tally.ABSTAIN;
		d.proposal.updatedAt = Date.now();
	});

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

// ── Pairwise comparison API ──

// GET /api/proposals/pair — get a random pair for Elo comparison
routes.get("/api/proposals/pair", (c) => {
	const spaceSlug = c.req.query("space_slug");
	if (!spaceSlug) return c.json({ error: "space_slug required" }, 400);

	const docs = listProposalDocs(spaceSlug);
	const eligible = docs.filter(d => d.doc.proposal.title && d.doc.proposal.status === "RANKING");
	if (eligible.length < 2) return c.json({ error: "Need at least 2 ranking proposals" }, 400);

	// Sortition: weight toward proposals with fewer comparisons
	const weights = eligible.map(d => {
		const pw = d.doc.pairwise || { elo: ELO_DEFAULT, comparisons: 0, wins: 0 };
		return 1 / (1 + pw.comparisons);
	});
	const totalWeight = weights.reduce((s, w) => s + w, 0);

	// Pick first proposal weighted by comparisons
	let r = Math.random() * totalWeight;
	let aIdx = 0;
	for (let i = 0; i < weights.length; i++) {
		r -= weights[i];
		if (r <= 0) { aIdx = i; break; }
	}

	// Pick second proposal (different from first), also weighted
	const remaining = eligible.filter((_, i) => i !== aIdx);
	const rWeights = remaining.map(d => {
		const pw = d.doc.pairwise || { elo: ELO_DEFAULT, comparisons: 0, wins: 0 };
		return 1 / (1 + pw.comparisons);
	});
	const rTotal = rWeights.reduce((s, w) => s + w, 0);
	let r2 = Math.random() * rTotal;
	let bIdx = 0;
	for (let i = 0; i < rWeights.length; i++) {
		r2 -= rWeights[i];
		if (r2 <= 0) { bIdx = i; break; }
	}

	return c.json({
		a: proposalToRest(eligible[aIdx].doc),
		b: proposalToRest(remaining[bIdx].doc),
	});
});

// POST /api/proposals/compare — record a pairwise comparison result
routes.post("/api/proposals/compare", async (c) => {
	const token = extractToken(c.req.raw.headers);
	if (!token) return c.json({ error: "Authentication required" }, 401);
	try { await verifyToken(token); } catch { return c.json({ error: "Invalid token" }, 401); }

	const body = await c.req.json();
	const { winnerId, loserId } = body;
	if (!winnerId || !loserId) return c.json({ error: "winnerId and loserId required" }, 400);
	if (winnerId === loserId) return c.json({ error: "Cannot compare proposal to itself" }, 400);

	// Find both proposals
	const allDocs = listAllProposalDocs();
	const winnerMatch = allDocs.find(d => d.doc.proposal.id === winnerId);
	const loserMatch = allDocs.find(d => d.doc.proposal.id === loserId);
	if (!winnerMatch || !loserMatch) return c.json({ error: "Proposal not found" }, 404);

	const winnerPw = winnerMatch.doc.pairwise || { elo: ELO_DEFAULT, comparisons: 0, wins: 0 };
	const loserPw = loserMatch.doc.pairwise || { elo: ELO_DEFAULT, comparisons: 0, wins: 0 };

	const result = computeElo(winnerPw.elo, loserPw.elo);
	const winnerDelta = result.winner - winnerPw.elo;
	const loserDelta = result.loser - loserPw.elo;

	// Update winner
	_syncServer!.changeDoc<ProposalDoc>(winnerMatch.docId, 'pairwise win', (d) => {
		if (!d.pairwise) {
			(d as any).pairwise = { elo: ELO_DEFAULT, comparisons: 0, wins: 0 };
		}
		d.pairwise.elo = result.winner;
		d.pairwise.comparisons += 1;
		d.pairwise.wins += 1;
	});

	// Update loser
	_syncServer!.changeDoc<ProposalDoc>(loserMatch.docId, 'pairwise loss', (d) => {
		if (!d.pairwise) {
			(d as any).pairwise = { elo: ELO_DEFAULT, comparisons: 0, wins: 0 };
		}
		d.pairwise.elo = result.loser;
		d.pairwise.comparisons += 1;
	});

	return c.json({
		ok: true,
		winner: { elo: result.winner, delta: winnerDelta },
		loser: { elo: result.loser, delta: loserDelta },
	});
});

// ── Demo page body (reused by / when space=demo, and /demo fallback) ──

function renderDemoBody(): string {
	return `
<div class="rd-page">
	<div class="rd-hero">
		<span class="rl-badge" style="background:#1e293b;color:#94a3b8;font-size:0.7rem;padding:0.25rem 0.75rem;display:inline-block;margin-bottom:0.75rem;border-radius:9999px">Interactive Demo</span>
		<h1>Live Community Polls</h1>
		<p>These polls are synced in real-time across the entire r* ecosystem via rSpace. Vote on options and watch tallies update live for everyone.</p>
	</div>

	<!-- ELI5: How rVote Works -->
	<div style="margin-bottom:2rem">
		<h2 style="text-align:center;font-size:1.1rem;font-weight:600;color:#cbd5e1;margin-bottom:1rem">How rVote Works</h2>
		<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:0.75rem">
			<div class="rd-card" style="border:2px solid rgba(249,115,22,0.35);background:linear-gradient(to bottom right,rgba(249,115,22,0.08),rgba(249,115,22,0.03));padding:1rem">
				<div style="display:flex;align-items:center;gap:0.4rem;margin-bottom:0.5rem">
					<div style="width:1.5rem;height:1.5rem;border-radius:9999px;background:#f97316;display:flex;align-items:center;justify-content:center;flex-shrink:0">
						<span style="color:white;font-weight:700;font-size:0.65rem">x&sup2;</span>
					</div>
					<h3 style="color:#fb923c;font-size:0.9rem;margin:0">Quadratic</h3>
				</div>
				<p style="font-size:0.8rem;margin:0;line-height:1.5">
					Voting more costs exponentially. 1 vote = 1 credit, 2 = 4, 3 = 9.
					<strong style="display:block;margin-top:0.35rem;color:#e2e8f0;font-size:0.78rem">No single voice can dominate.</strong>
				</p>
			</div>
			<div class="rd-card" style="border:2px solid rgba(59,130,246,0.35);background:linear-gradient(to bottom right,rgba(59,130,246,0.08),rgba(59,130,246,0.03));padding:1rem">
				<div style="display:flex;align-items:center;gap:0.4rem;margin-bottom:0.5rem">
					<div style="width:1.5rem;height:1.5rem;border-radius:9999px;background:#3b82f6;display:flex;align-items:center;justify-content:center;flex-shrink:0">
						<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"/></svg>
					</div>
					<h3 style="color:#60a5fa;font-size:0.9rem;margin:0">Reddit-style</h3>
				</div>
				<p style="font-size:0.8rem;margin:0;line-height:1.5">
					Upvote or downvote proposals. Scores aggregate from all votes.
					<strong style="display:block;margin-top:0.35rem;color:#e2e8f0;font-size:0.78rem">Best ideas rise to the top.</strong>
				</p>
			</div>
			<div class="rd-card" style="border:2px solid rgba(168,85,247,0.35);background:linear-gradient(to bottom right,rgba(168,85,247,0.08),rgba(168,85,247,0.03));padding:1rem">
				<div style="display:flex;align-items:center;gap:0.4rem;margin-bottom:0.5rem">
					<div style="width:1.5rem;height:1.5rem;border-radius:9999px;background:#a855f7;display:flex;align-items:center;justify-content:center;flex-shrink:0">
						<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
					</div>
					<h3 style="color:#c084fc;font-size:0.9rem;margin:0">Vote Decay</h3>
				</div>
				<p style="font-size:0.8rem;margin:0;line-height:1.5">
					Votes fade after 30&ndash;60 days. Old support expires naturally.
					<strong style="display:block;margin-top:0.35rem;color:#e2e8f0;font-size:0.78rem">Rankings stay fresh.</strong>
				</p>
			</div>
		</div>
	</div>

	<!-- Conviction Voting Simulator -->
	<div style="margin-bottom:2.5rem">
		<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem;flex-wrap:wrap;gap:0.5rem">
			<div>
				<span class="rd-badge rd-badge--blue" style="margin-bottom:0.25rem">Try It</span>
				<h2 style="font-size:1.1rem;font-weight:600;color:#cbd5e1;margin:0.25rem 0 0">Conviction Voting Simulator</h2>
			</div>
			<div id="rd-credit-budget" style="display:flex;align-items:center;gap:0.5rem;background:#0f172a;border:1px solid #1e293b;border-radius:8px;padding:0.4rem 0.75rem">
				<span style="font-size:0.75rem;color:#64748b">Your credits:</span>
				<span id="rd-credits" style="font-weight:700;color:#22c55e;font-family:monospace;font-size:0.95rem">50</span>
				<span style="font-size:0.7rem;color:#475569">/ 50</span>
			</div>
		</div>
		<p style="font-size:0.8rem;color:#64748b;margin:0 0 1rem">
			Upvote proposals to rank them. Each additional vote costs quadratically more credits (1, 4, 9, 16...). Proposals re-rank by score in real-time.
		</p>
		<div id="rd-sim-proposals"></div>
		<div style="text-align:center;margin-top:0.75rem">
			<button id="rd-sim-reset" class="rd-btn" style="font-size:0.75rem">Reset Simulator</button>
		</div>
	</div>

	<!-- Live Synced Polls -->
	<div style="margin-bottom:0.5rem">
		<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem;flex-wrap:wrap;gap:0.5rem">
			<div>
				<span class="rd-badge rd-badge--green" style="margin-bottom:0.25rem">Live</span>
				<h2 style="font-size:1.1rem;font-weight:600;color:#cbd5e1;margin:0.25rem 0 0">Community Polls</h2>
			</div>
			<div class="rd-toolbar" style="margin:0">
				<span id="rd-conn-badge" class="rd-status rd-status--disconnected">Connecting</span>
				<button id="rd-reset-btn" class="rd-btn" disabled style="font-size:0.75rem">Reset Demo</button>
			</div>
		</div>
		<p style="font-size:0.8rem;color:#64748b;margin:0 0 1rem">
			These polls sync across all r* apps in real-time via WebSocket.
		</p>
	</div>

	<div id="rd-loading" class="rd-loading">
		<div class="rd-loading-spinner"></div>
		Connecting to rSpace...
	</div>

	<div id="rd-empty" class="rd-empty" style="display:none">
		<div class="rd-empty-icon">🗳</div>
		No polls found. Click <strong>Reset Demo</strong> to seed some sample polls.
	</div>

	<div id="rd-polls-container"></div>

	<!-- CTA Card -->
	<div style="margin:2.5rem auto;max-width:640px">
		<div class="rl-card" style="border:2px solid rgba(249,115,22,0.25);background:linear-gradient(to bottom right,rgba(249,115,22,0.08),rgba(249,115,22,0.03));text-align:center;padding:2rem 1.5rem">
			<h2 style="font-size:1.25rem;font-weight:700;color:#e2e8f0;margin:0 0 0.75rem">Ready to try it for real?</h2>
			<p style="color:#94a3b8;max-width:480px;margin:0 auto 1.5rem;line-height:1.6;font-size:0.95rem">
				Create a Space for your community to start ranking and voting on proposals. Invite members and allot credits to get started.
			</p>
			<div style="display:flex;gap:0.75rem;justify-content:center;flex-wrap:wrap">
				<a href="/rids" class="rl-cta-primary" style="background:linear-gradient(to right,#f97316,#ea580c);color:white;text-decoration:none;padding:0.6rem 1.5rem;border-radius:0.5rem;font-weight:600;font-size:0.875rem">Create Account</a>
				<a href="/rvote" class="rl-cta-secondary" style="text-decoration:none;padding:0.6rem 1.5rem;border-radius:0.5rem;font-weight:600;font-size:0.875rem;border:1px solid rgba(148,163,184,0.3);color:#94a3b8">Learn More</a>
			</div>
		</div>
	</div>

	<div class="rd-footer">
		<a href="/rvote">&larr; Back to rVote</a>
	</div>
</div>`;
}

// ── Page routes ──

// Main route — serves demo page when space=demo, dashboard otherwise
routes.get("/", (c) => {
	const space = c.req.param("space") || "demo";

	if (space === "demo") {
		return c.html(renderShell({
			title: "rVote Demo — Interactive Polls | rSpace",
			moduleId: "rvote",
			spaceSlug: "demo",
			modules: getModuleInfoList(),
			theme: "dark",
			body: renderDemoBody(),
			scripts: `<script type="module" src="/modules/rvote/vote-demo.js"></script>`,
			styles: `<link rel="stylesheet" href="/modules/rvote/vote.css">`,
		}));
	}

	return c.html(renderShell({
		title: `${space} — Vote | rSpace`,
		moduleId: "rvote",
		spaceSlug: space,
		modules: getModuleInfoList(),
		theme: "dark",
		body: `<folk-vote-dashboard space="${space}"></folk-vote-dashboard>`,
		scripts: `<script type="module" src="/modules/rvote/folk-vote-dashboard.js?v=2"></script>`,
		styles: `<link rel="stylesheet" href="/modules/rvote/vote.css">`,
	}));
});

export function getActiveProposalsForMI(space: string, limit = 5): { id: string; title: string; status: string; score: number; voteCount: number; createdAt: number }[] {
	if (!_syncServer) return [];
	const items: { id: string; title: string; status: string; score: number; voteCount: number; createdAt: number }[] = [];
	for (const docId of _syncServer.listDocs()) {
		if (!docId.startsWith(`${space}:vote:proposals:`)) continue;
		const doc = _syncServer.getDoc<ProposalDoc>(docId);
		if (!doc?.proposal) continue;
		const p = doc.proposal;
		items.push({ id: p.id, title: p.title, status: p.status, score: p.score || 0, voteCount: Object.keys(doc.votes || {}).length, createdAt: p.createdAt });
	}
	return items.sort((a, b) => b.createdAt - a.createdAt).slice(0, limit);
}

export const voteModule: RSpaceModule = {
	id: "rvote",
	name: "rVote",
	icon: "🗳",
	description: "Conviction voting engine for collaborative governance",
	scoping: { defaultScope: 'space', userConfigurable: false },
	docSchemas: [{ pattern: '{space}:vote:proposals:{proposalId}', description: 'Proposal with votes', init: proposalSchema.init }],
	routes,
	standaloneDomain: "rvote.online",
	landingPage: renderLanding,
	seedTemplate: seedDemoIfEmpty,
	async onInit(ctx) {
		_syncServer = ctx.syncServer;
		seedDemoIfEmpty();
	},
	feeds: [
		{
			id: "proposals",
			name: "Proposals",
			kind: "governance",
			description: "Active proposals with conviction scores and vote tallies",
			filterable: true,
		},
		{
			id: "decisions",
			name: "Decision Outcomes",
			kind: "governance",
			description: "Passed and failed proposals — governance decisions made",
		},
	],
	acceptsFeeds: ["economic", "data"],
	outputPaths: [
		{ path: "proposals", name: "Proposals", icon: "📜", description: "Governance proposals for conviction voting" },
		{ path: "ballots", name: "Ballots", icon: "🗳️", description: "Voting ballots and results" },
		{ path: "rank", name: "Rank", icon: "🎲", description: "Pairwise Elo ranking via sortition" },
	],
	onboardingActions: [
		{ label: "Create a Proposal", icon: "🗳️", description: "Start a governance vote", type: 'create', href: '/{space}/rvote' },
	],
};
