/**
 * 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 { verifyEncryptIDToken, extractToken } from "@encryptid/sdk/server";
import { renderLanding } from "./landing";
import type { SyncServer } from '../../server/local-first/sync-server';
import { proposalSchema, proposalDocId } 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;
	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),
		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
	if (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);
	}

	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 verifyEncryptIDToken(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 verifyEncryptIDToken(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);

	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 verifyEncryptIDToken(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 verifyEncryptIDToken(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 });
});

// ── Page routes ──

// Demo page — interactive polls with live sync
routes.get("/demo", (c) => {
	return c.html(renderShell({
		title: "rVote Demo — Interactive Polls | rSpace",
		moduleId: "rvote",
		spaceSlug: "demo",
		modules: getModuleInfoList(),
		theme: "dark",
		body: `
<div class="rd-page">
	<div class="rd-hero">
		<h1>rVote Demo</h1>
		<p>Vote on live proposals synced across the r* ecosystem. Changes appear in real-time for everyone.</p>
	</div>

	<div class="rd-toolbar">
		<span id="rd-conn-badge" class="rd-status rd-status--disconnected">Connecting</span>
		<button id="rd-reset-btn" class="rd-btn" disabled>Reset Demo</button>
		<a href="/rvote" class="rd-btn" style="text-decoration:none;margin-left:auto">&larr; About rVote</a>
	</div>

	<div id="rd-loading" class="rd-loading">
		<div class="rd-loading-spinner"></div>
		Loading polls...
	</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>

	<div class="rd-footer">
		<a href="/rvote">&larr; Back to rVote</a>
	</div>
</div>`,
		scripts: `<script type="module" src="/modules/rvote/vote-demo.js"></script>`,
		styles: `<link rel="stylesheet" href="/modules/rvote/vote.css">`,
	}));
});

// Dashboard — full voting app with spaces, proposals, conviction voting
routes.get("/", (c) => {
	const space = c.req.param("space") || "demo";
	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"></script>`,
		styles: `<link rel="stylesheet" href="/modules/rvote/vote.css">`,
	}));
});

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" },
	],
};
