/**
 * Vote module — conviction voting engine.
 *
 * Credit-weighted conviction voting for collaborative governance.
 * Spaces run ranked proposals with configurable parameters.
 */

import { Hono } from "hono";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { sql } from "../../shared/db/pool";
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";

const routes = new Hono();

// ── DB initialization ──
const SCHEMA_SQL = readFileSync(resolve(import.meta.dir, "db/schema.sql"), "utf-8");

async function initDB() {
	try {
		await sql.unsafe(SCHEMA_SQL);
		console.log("[Vote] DB schema initialized");
	} catch (e) {
		console.error("[Vote] DB init error:", e);
	}
}

async function seedDemoIfEmpty() {
	try {
		const count = await sql.unsafe("SELECT count(*)::int as cnt FROM rvote.spaces");
		if (parseInt(count[0].cnt) > 0) return;

		// Create demo user
		const user = await sql.unsafe(
			`INSERT INTO rvote.users (did, username) VALUES ('did:demo:seed', 'demo')
			 ON CONFLICT (did) DO UPDATE SET username = 'demo' RETURNING id`
		);
		const userId = user[0].id;

		// Create voting space (matches standalone rVote settings)
		await sql.unsafe(
			`INSERT INTO rvote.spaces (slug, name, description, owner_did, visibility, promotion_threshold, voting_period_days, credits_per_day, max_credits, starting_credits)
			 VALUES ('community', 'Community Governance', 'Proposals for the rSpace ecosystem', 'did:demo:seed', 'public_read', 100, 7, 10, 500, 50)`
		);

		// Seed proposals in various states
		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 row = await sql.unsafe(
				`INSERT INTO rvote.proposals (space_slug, author_id, title, description, status, score)
				 VALUES ('community', $1, $2, $3, $4, $5) RETURNING id`,
				[userId, p.title, p.desc, p.status, p.score]
			);

			if (p.status === "VOTING") {
				await sql.unsafe(
					`UPDATE rvote.proposals SET voting_ends_at = NOW() + INTERVAL '5 days', final_yes = 5, final_no = 2 WHERE id = $1`,
					[row[0].id]
				);
			} else if (p.status === "PASSED") {
				await sql.unsafe(
					`UPDATE rvote.proposals SET final_yes = 12, final_no = 3, final_abstain = 2 WHERE id = $1`,
					[row[0].id]
				);
			} else if (p.status === "FAILED") {
				await sql.unsafe(
					`UPDATE rvote.proposals SET final_yes = 2, final_no = 8, final_abstain = 1 WHERE id = $1`,
					[row[0].id]
				);
			}
		}

		console.log("[Vote] Demo data seeded: 1 space, 5 proposals");
	} catch (e) {
		console.error("[Vote] Seed error:", e);
	}
}

initDB().then(seedDemoIfEmpty);

// ── Helper: get or create user by DID ──
async function getOrCreateUser(did: string, username?: string) {
	const rows = await sql.unsafe(
		`INSERT INTO rvote.users (did, username) VALUES ($1, $2)
		 ON CONFLICT (did) DO UPDATE SET username = COALESCE($2, rvote.users.username)
		 RETURNING *`,
		[did, username || null]
	);
	return rows[0];
}

// ── Helper: calculate effective weight with decay ──
function getEffectiveWeight(weight: number, createdAt: Date): number {
	const ageMs = Date.now() - createdAt.getTime();
	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 ──
async function recalcScore(proposalId: string) {
	const votes = await sql.unsafe(
		"SELECT weight, created_at FROM rvote.votes WHERE proposal_id = $1",
		[proposalId]
	);
	let score = 0;
	for (const v of votes) {
		score += getEffectiveWeight(v.weight, new Date(v.created_at));
	}
	await sql.unsafe(
		"UPDATE rvote.proposals SET score = $1, updated_at = NOW() WHERE id = $2",
		[score, proposalId]
	);
	return score;
}

// ── Spaces API ──

// GET /api/spaces — list spaces
routes.get("/api/spaces", async (c) => {
	const rows = await sql.unsafe(
		"SELECT * FROM rvote.spaces ORDER BY created_at DESC LIMIT 50"
	);
	return c.json({ spaces: rows });
});

// 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_read" } = 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);

	try {
		const rows = await sql.unsafe(
			`INSERT INTO rvote.spaces (slug, name, description, owner_did, visibility)
			 VALUES ($1, $2, $3, $4, $5) RETURNING *`,
			[slug, name, description || null, claims.sub, visibility]
		);
		return c.json(rows[0], 201);
	} catch (e: any) {
		if (e.code === "23505") return c.json({ error: "Space already exists" }, 409);
		throw e;
	}
});

// GET /api/spaces/:slug — space detail
routes.get("/api/spaces/:slug", async (c) => {
	const slug = c.req.param("slug");
	const rows = await sql.unsafe("SELECT * FROM rvote.spaces WHERE slug = $1", [slug]);
	if (rows.length === 0) return c.json({ error: "Space not found" }, 404);
	return c.json(rows[0]);
});

// ── Proposals API ──

// GET /api/proposals — list proposals (query: space_slug, status)
routes.get("/api/proposals", async (c) => {
	const { space_slug, status, limit = "50", offset = "0" } = c.req.query();
	const conditions: string[] = [];
	const params: any[] = [];
	let idx = 1;

	if (space_slug) {
		conditions.push(`space_slug = $${idx}`);
		params.push(space_slug);
		idx++;
	}
	if (status) {
		conditions.push(`status = $${idx}`);
		params.push(status);
		idx++;
	}

	const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
	const rows = await sql.unsafe(
		`SELECT * FROM rvote.proposals ${where} ORDER BY score DESC, created_at DESC LIMIT ${Math.min(parseInt(limit), 100)} OFFSET ${parseInt(offset) || 0}`,
		params
	);
	return c.json({ proposals: rows });
});

// 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 space = await sql.unsafe("SELECT slug FROM rvote.spaces WHERE slug = $1", [space_slug]);
	if (space.length === 0) return c.json({ error: "Space not found" }, 404);

	const user = await getOrCreateUser(claims.sub, claims.username);
	const rows = await sql.unsafe(
		`INSERT INTO rvote.proposals (space_slug, author_id, title, description)
		 VALUES ($1, $2, $3, $4) RETURNING *`,
		[space_slug, user.id, title, description || null]
	);
	return c.json(rows[0], 201);
});

// GET /api/proposals/:id — proposal detail
routes.get("/api/proposals/:id", async (c) => {
	const id = c.req.param("id");
	const rows = await sql.unsafe(
		`SELECT p.*,
			(SELECT count(*) FROM rvote.votes WHERE proposal_id = p.id) as vote_count
		 FROM rvote.proposals p WHERE p.id = $1`,
		[id]
	);
	if (rows.length === 0) return c.json({ error: "Proposal not found" }, 404);
	return c.json(rows[0]);
});

// 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;

	// Verify proposal is in RANKING
	const proposal = await sql.unsafe(
		"SELECT * FROM rvote.proposals WHERE id = $1",
		[id]
	);
	if (proposal.length === 0) return c.json({ error: "Proposal not found" }, 404);
	if (proposal[0].status !== "RANKING") return c.json({ error: "Proposal not in ranking phase" }, 400);

	const user = await getOrCreateUser(claims.sub, claims.username);
	const creditCost = weight * weight; // quadratic cost

	// Upsert vote
	await sql.unsafe(
		`INSERT INTO rvote.votes (user_id, proposal_id, weight, credit_cost, decays_at)
		 VALUES ($1, $2, $3, $4, NOW() + INTERVAL '30 days')
		 ON CONFLICT (user_id, proposal_id)
		 DO UPDATE SET weight = $3, credit_cost = $4, created_at = NOW(), decays_at = NOW() + INTERVAL '30 days'`,
		[user.id, id, weight, creditCost]
	);

	// Recalculate score and check for promotion
	const score = await recalcScore(id);
	const space = await sql.unsafe(
		"SELECT * FROM rvote.spaces WHERE slug = $1",
		[proposal[0].space_slug]
	);
	const threshold = space[0]?.promotion_threshold || 100;

	if (score >= threshold && proposal[0].status === "RANKING") {
		const votingDays = space[0]?.voting_period_days || 7;
		await sql.unsafe(
			`UPDATE rvote.proposals SET status = 'VOTING', voting_ends_at = NOW() + ($1 || ' days')::INTERVAL, updated_at = NOW() WHERE id = $2`,
			[votingDays, id]
		);
	}

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

	const proposal = await sql.unsafe("SELECT * FROM rvote.proposals WHERE id = $1", [id]);
	if (proposal.length === 0) return c.json({ error: "Proposal not found" }, 404);
	if (proposal[0].status !== "VOTING") return c.json({ error: "Proposal not in voting phase" }, 400);

	const user = await getOrCreateUser(claims.sub, claims.username);
	await sql.unsafe(
		`INSERT INTO rvote.final_votes (user_id, proposal_id, vote)
		 VALUES ($1, $2, $3)
		 ON CONFLICT (user_id, proposal_id) DO UPDATE SET vote = $3`,
		[user.id, id, vote]
	);

	// Update counts
	const counts = await sql.unsafe(
		`SELECT vote, count(*) as cnt FROM rvote.final_votes WHERE proposal_id = $1 GROUP BY vote`,
		[id]
	);
	const tally: Record<string, number> = { YES: 0, NO: 0, ABSTAIN: 0 };
	for (const row of counts) tally[row.vote] = parseInt(row.cnt);

	await sql.unsafe(
		"UPDATE rvote.proposals SET final_yes = $1, final_no = $2, final_abstain = $3, updated_at = NOW() WHERE id = $4",
		[tally.YES, tally.NO, tally.ABSTAIN, id]
	);

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

// ── Page route ──
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 },
	routes,
	standaloneDomain: "rvote.online",
	landingPage: renderLanding,
	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" },
	],
};
