import { FolkShape } from "./folk-shape";
import { css, html } from "./tags";
import type { PortDescriptor } from "./data-types";

const USER_ID_KEY = "folk-choice-userid";
const USER_NAME_KEY = "folk-choice-username";

const styles = css`
	:host {
		background: var(--rs-bg-surface, #fff);
		color: var(--rs-text-primary, #1e293b);
		border-radius: 8px;
		box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
		min-width: 320px;
		min-height: 300px;
	}

	.header {
		display: flex;
		align-items: center;
		justify-content: space-between;
		padding: 8px 12px;
		background: #0d9488;
		color: white;
		border-radius: 8px 8px 0 0;
		font-size: 12px;
		font-weight: 600;
		cursor: move;
	}

	.header-title {
		display: flex;
		align-items: center;
		gap: 6px;
	}

	.header-actions {
		display: flex;
		gap: 4px;
	}

	.header-actions button {
		background: transparent;
		border: none;
		color: white;
		cursor: pointer;
		padding: 2px 6px;
		border-radius: 4px;
		font-size: 14px;
	}

	.header-actions button:hover {
		background: rgba(255, 255, 255, 0.2);
	}

	.body {
		display: flex;
		flex-direction: column;
		height: calc(100% - 36px);
		overflow: hidden;
	}

	.mode-tabs {
		display: flex;
		border-bottom: 1px solid var(--rs-border, rgba(255, 255, 255, 0.1));
	}

	.mode-tab {
		flex: 1;
		padding: 6px 8px;
		text-align: center;
		font-size: 11px;
		font-weight: 500;
		cursor: pointer;
		border: none;
		background: transparent;
		color: var(--rs-text-muted, #64748b);
		transition: all 0.15s;
	}

	.mode-tab.active {
		color: #0d9488;
		border-bottom: 2px solid #0d9488;
		background: rgba(13, 148, 136, 0.1);
	}

	.options-list {
		flex: 1;
		overflow-y: auto;
		padding: 8px 12px;
	}

	.option-row {
		display: flex;
		align-items: center;
		gap: 8px;
		padding: 8px;
		border-radius: 6px;
		margin-bottom: 6px;
		cursor: pointer;
		transition: background 0.15s;
		position: relative;
		overflow: hidden;
	}

	.option-row:hover {
		background: var(--rs-bg-hover, rgba(255, 255, 255, 0.05));
	}

	.option-row.voted {
		background: rgba(13, 148, 136, 0.1);
	}

	.bar-bg {
		position: absolute;
		left: 0;
		top: 0;
		bottom: 0;
		border-radius: 6px;
		opacity: 0.12;
		transition: width 0.4s ease;
	}

	.option-dot {
		width: 8px;
		height: 8px;
		border-radius: 50%;
		flex-shrink: 0;
		z-index: 1;
	}

	.option-label {
		flex: 1;
		font-size: 13px;
		z-index: 1;
		color: var(--rs-text-primary, #e2e8f0);
	}

	.option-count {
		font-size: 12px;
		color: var(--rs-text-muted, #64748b);
		font-variant-numeric: tabular-nums;
		z-index: 1;
		min-width: 20px;
		text-align: right;
	}

	.option-pct {
		font-size: 11px;
		font-weight: 600;
		font-variant-numeric: tabular-nums;
		z-index: 1;
		min-width: 32px;
		text-align: right;
	}

	.qv-controls {
		display: flex;
		align-items: center;
		gap: 4px;
		z-index: 1;
	}

	.qv-btn {
		width: 22px;
		height: 22px;
		border: 1px solid var(--rs-border, rgba(255, 255, 255, 0.1));
		border-radius: 4px;
		background: var(--rs-bg-surface, #1e293b);
		cursor: pointer;
		font-size: 12px;
		display: flex;
		align-items: center;
		justify-content: center;
		color: var(--rs-text-secondary, #94a3b8);
	}

	.qv-btn:hover { background: var(--rs-bg-hover, rgba(255, 255, 255, 0.05)); }
	.qv-btn:disabled { opacity: 0.3; cursor: default; }

	.qv-count {
		font-size: 12px;
		font-variant-numeric: tabular-nums;
		min-width: 14px;
		text-align: center;
		color: var(--rs-text-primary, #e2e8f0);
	}

	.budget-bar {
		padding: 4px 12px;
		font-size: 11px;
		color: var(--rs-text-muted, #64748b);
		border-top: 1px solid var(--rs-border, rgba(255, 255, 255, 0.1));
		text-align: center;
	}

	.budget-bar .used {
		font-weight: 600;
		color: #0d9488;
	}

	.voters-count {
		padding: 4px 12px;
		font-size: 11px;
		color: var(--rs-text-secondary, #94a3b8);
		text-align: center;
	}

	.add-form {
		display: flex;
		gap: 6px;
		padding: 8px 12px;
		border-top: 1px solid var(--rs-border, rgba(255, 255, 255, 0.1));
	}

	.add-input {
		flex: 1;
		border: 1px solid var(--rs-input-border, #e2e8f0);
		border-radius: 6px;
		padding: 6px 10px;
		font-size: 12px;
		outline: none;
		background: var(--rs-input-bg, #fff);
		color: var(--rs-input-text, inherit);
	}

	.add-input:focus { border-color: #0d9488; }

	.add-btn {
		background: #0d9488;
		color: white;
		border: none;
		border-radius: 6px;
		padding: 6px 12px;
		cursor: pointer;
		font-size: 12px;
		font-weight: 500;
	}

	.add-btn:hover { background: #0f766e; }

	.username-prompt {
		padding: 16px;
		text-align: center;
	}

	.username-prompt p {
		font-size: 13px;
		color: #64748b;
		margin: 0 0 8px;
	}

	.username-input {
		border: 1px solid var(--rs-input-border, #e2e8f0);
		border-radius: 6px;
		padding: 8px 12px;
		font-size: 13px;
		outline: none;
		background: var(--rs-input-bg, #fff);
		color: var(--rs-input-text, inherit);
		width: 100%;
		box-sizing: border-box;
		margin-bottom: 8px;
	}

	.username-btn {
		background: #0d9488;
		color: white;
		border: none;
		border-radius: 6px;
		padding: 8px 16px;
		cursor: pointer;
		font-weight: 500;
		font-size: 13px;
	}

	.wrapper { position: relative; height: 100%; }
	.results-drawer {
		position: absolute; top: 0; left: 100%; width: 300px; height: 100%;
		background: var(--rs-bg-surface, #fff); border-radius: 0 8px 8px 0;
		box-shadow: 4px 0 12px rgba(0,0,0,0.08);
		overflow-y: auto; display: none; flex-direction: column;
		font-size: 12px; z-index: 10;
	}
	.drawer-open .results-drawer { display: flex; }
	.drawer-section { padding: 10px 12px; border-bottom: 1px solid #e2e8f0; }
	.drawer-heading {
		font-size: 10px; font-weight: 600; text-transform: uppercase;
		letter-spacing: 0.5px; color: #94a3b8; margin-bottom: 6px;
	}
	.stat-row { display: flex; justify-content: space-between; padding: 3px 0; font-size: 11px; }
	.stat-label { color: #64748b; }
	.stat-value { font-weight: 600; color: #1e293b; font-variant-numeric: tabular-nums; }
	.drawer-bar-row { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; }
	.drawer-bar-label { font-size: 11px; min-width: 60px; color: #1e293b; }
	.drawer-bar-bg { flex: 1; height: 12px; background: #f1f5f9; border-radius: 3px; overflow: hidden; }
	.drawer-bar-fill { height: 100%; border-radius: 3px; transition: width 0.3s; }
	.drawer-bar-val { font-size: 10px; font-weight: 600; min-width: 24px; text-align: right; font-variant-numeric: tabular-nums; }
	.participant-row { display: flex; align-items: center; gap: 6px; padding: 2px 0; font-size: 11px; color: #475569; }
	.participant-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
	.drawer-toggle.active { background: rgba(255,255,255,0.3); }
	.settings-toggle.active { background: rgba(255,255,255,0.3); }
	.settings-panel { display: none; flex-direction: column; gap: 12px; padding: 12px; overflow-y: auto; height: calc(100% - 36px); }
	.settings-open .settings-panel { display: flex; }
	.settings-open .body { display: none !important; }
	.settings-open .results-drawer { display: none !important; }
	.settings-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: #94a3b8; margin-bottom: 6px; }
	.settings-input { width: 100%; box-sizing: border-box; border: 1px solid var(--rs-input-border, #e2e8f0); border-radius: 6px; padding: 6px 10px; font-size: 12px; outline: none; background: var(--rs-input-bg, #fff); color: var(--rs-input-text, inherit); }
	.settings-input:focus { border-color: #0d9488; }
	.settings-item { display: flex; align-items: center; gap: 6px; padding: 4px 0; }
	.settings-item input[type="text"] { flex: 1; border: 1px solid #e2e8f0; border-radius: 4px; padding: 4px 6px; font-size: 11px; outline: none; min-width: 0; }
	.settings-item input[type="color"] { width: 24px; height: 24px; border: none; border-radius: 4px; padding: 0; cursor: pointer; background: none; }
	.settings-item input[type="number"] { width: 60px; border: 1px solid #e2e8f0; border-radius: 4px; padding: 4px 6px; font-size: 11px; outline: none; }
	.settings-item .remove-btn { background: none; border: none; color: #94a3b8; cursor: pointer; font-size: 14px; padding: 2px 4px; border-radius: 4px; flex-shrink: 0; }
	.settings-item .remove-btn:hover { color: #ef4444; background: #fef2f2; }
	.settings-danger { background: none; border: 1px solid #fca5a5; color: #ef4444; border-radius: 6px; padding: 6px 12px; cursor: pointer; font-size: 11px; width: 100%; margin-top: 4px; }
	.settings-danger:hover { background: #fef2f2; }
	.settings-done { background: #0d9488; color: white; border: none; border-radius: 6px; padding: 8px 16px; cursor: pointer; font-size: 12px; font-weight: 500; width: 100%; margin-top: auto; }
	.settings-done:hover { background: #0f766e; }
`;

// -- Data types --

export interface VoteOption {
	id: string;
	label: string;
	color: string;
}

export interface UserVote {
	userId: string;
	userName: string;
	allocations: Record<string, number>;
	timestamp: number;
}

export type VoteMode = "plurality" | "approval" | "quadratic";

// -- Pure aggregation functions --

export function tallyVotes(
	votes: UserVote[],
	options: VoteOption[],
): Map<string, number> {
	const tally = new Map<string, number>();
	for (const opt of options) tally.set(opt.id, 0);
	for (const v of votes) {
		for (const [optId, count] of Object.entries(v.allocations)) {
			if (tally.has(optId)) {
				tally.set(optId, tally.get(optId)! + count);
			}
		}
	}
	return tally;
}

export function quadraticCost(allocations: Record<string, number>): number {
	let total = 0;
	for (const k of Object.values(allocations)) {
		total += k * k;
	}
	return total;
}

export function giniCoefficient(tally: Map<string, number>): number {
	const vals = [...tally.values()].sort((a, b) => a - b);
	const n = vals.length;
	if (n === 0) return 0;
	const total = vals.reduce((a, b) => a + b, 0);
	if (total === 0) return 0;
	let sum = 0;
	for (let i = 0; i < n; i++) {
		sum += (2 * (i + 1) - n - 1) * vals[i];
	}
	return sum / (n * total);
}

export function effectiveVotes(
	votes: UserVote[],
	options: VoteOption[],
): Map<string, number> {
	const result = new Map<string, number>();
	for (const opt of options) result.set(opt.id, 0);
	for (const v of votes) {
		for (const [optId, count] of Object.entries(v.allocations)) {
			if (result.has(optId)) {
				result.set(optId, result.get(optId)! + Math.sqrt(count));
			}
		}
	}
	return result;
}

// -- Component --

declare global {
	interface HTMLElementTagNameMap {
		"folk-choice-vote": FolkChoiceVote;
	}
}

const DEFAULT_COLORS = ["#3b82f6", "#22c55e", "#f59e0b", "#ef4444", "#8b5cf6", "#ec4899", "#06b6d4", "#f97316"];

export class FolkChoiceVote extends FolkShape {
	static override tagName = "folk-choice-vote";

	static override portDescriptors: PortDescriptor[] = [
		{ name: "results-out", type: "json", direction: "output" },
		{ name: "decision-out", type: "json", direction: "output" },
		{ name: "vote-complete", type: "trigger", direction: "output" },
	];

	static {
		const sheet = new CSSStyleSheet();
		const parentRules = Array.from(FolkShape.styles.cssRules).map((r) => r.cssText).join("\n");
		const childRules = Array.from(styles.cssRules).map((r) => r.cssText).join("\n");
		sheet.replaceSync(`${parentRules}\n${childRules}`);
		this.styles = sheet;
	}

	#title = "Quick Poll";
	#options: VoteOption[] = [];
	#mode: VoteMode = "plurality";
	#budget = 100;
	#votes: UserVote[] = [];
	#userId = "";
	#userName = "";
	#drawerOpen = false;
	#settingsOpen = false;

	// DOM refs
	#wrapperEl: HTMLElement | null = null;
	#bodyEl: HTMLElement | null = null;
	#optionsEl: HTMLElement | null = null;
	#budgetEl: HTMLElement | null = null;
	#votersEl: HTMLElement | null = null;
	#drawerEl: HTMLElement | null = null;
	#settingsEl: HTMLElement | null = null;

	get title() { return this.#title; }
	set title(v: string) {
		this.#title = v;
		this.requestUpdate("title");
	}

	get options() { return this.#options; }
	set options(v: VoteOption[]) {
		this.#options = v;
		this.#render();
		this.requestUpdate("options");
	}

	get mode() { return this.#mode; }
	set mode(v: VoteMode) {
		this.#mode = v;
		this.#render();
		this.requestUpdate("mode");
	}

	get budget() { return this.#budget; }
	set budget(v: number) {
		this.#budget = v;
		this.#render();
		this.requestUpdate("budget");
	}

	get votes() { return this.#votes; }
	set votes(v: UserVote[]) {
		this.#votes = v;
		this.#render();
		this.requestUpdate("votes");
	}

	#ensureIdentity(): boolean {
		if (this.#userId && this.#userName) return true;
		this.#userId = localStorage.getItem(USER_ID_KEY) || "";
		this.#userName = localStorage.getItem(USER_NAME_KEY) || localStorage.getItem("rspace-username") || "";
		if (!this.#userId) {
			this.#userId = crypto.randomUUID().slice(0, 8);
			localStorage.setItem(USER_ID_KEY, this.#userId);
		}
		return !!this.#userName;
	}

	#setUserName(name: string) {
		this.#userName = name;
		localStorage.setItem(USER_NAME_KEY, name);
		localStorage.setItem("rspace-username", name);
	}

	#getUserVote(): UserVote | undefined {
		return this.#votes.find((v) => v.userId === this.#userId);
	}

	override createRenderRoot() {
		const root = super.createRenderRoot();
		this.initPorts();
		this.#ensureIdentity();

		const wrapper = document.createElement("div");
		wrapper.className = "wrapper";
		wrapper.innerHTML = html`
			<div class="header">
				<span class="header-title">
					<span>&#x2611;</span>
					<span class="title-text">Poll</span>
				</span>
				<div class="header-actions">
					<button class="settings-toggle" title="Settings">&#x2699;</button>
					<button class="drawer-toggle" title="Results &amp; Stats">&#x1F4CA;</button>
					<button class="close-btn" title="Close">&times;</button>
				</div>
			</div>
			<div class="body">
				<div class="mode-tabs">
					<button class="mode-tab active" data-mode="plurality">Plurality</button>
					<button class="mode-tab" data-mode="approval">Approval</button>
					<button class="mode-tab" data-mode="quadratic">Quadratic</button>
				</div>
				<div class="options-list"></div>
				<div class="budget-bar" style="display:none;"></div>
				<div class="voters-count"></div>
				<div class="add-form">
					<input type="text" class="add-input" placeholder="Add option..." />
					<button class="add-btn">Add</button>
				</div>
			</div>
			<div class="results-drawer"></div>
			<div class="settings-panel"></div>
			<div class="username-prompt" style="display: none;">
				<p>Enter your name to vote:</p>
				<input type="text" class="username-input" placeholder="Your name..." />
				<button class="username-btn">Join</button>
			</div>
		`;

		const slot = root.querySelector("slot");
		const containerDiv = slot?.parentElement as HTMLElement;
		if (containerDiv) containerDiv.replaceWith(wrapper);

		this.#wrapperEl = wrapper;
		this.#bodyEl = wrapper.querySelector(".body") as HTMLElement;
		this.#optionsEl = wrapper.querySelector(".options-list") as HTMLElement;
		this.#budgetEl = wrapper.querySelector(".budget-bar") as HTMLElement;
		this.#votersEl = wrapper.querySelector(".voters-count") as HTMLElement;
		this.#drawerEl = wrapper.querySelector(".results-drawer") as HTMLElement;
		const titleEl = wrapper.querySelector(".title-text") as HTMLElement;

		const drawerToggle = wrapper.querySelector(".drawer-toggle") as HTMLButtonElement;
		drawerToggle.addEventListener("click", (e) => {
			e.stopPropagation();
			this.#drawerOpen = !this.#drawerOpen;
			this.#wrapperEl!.classList.toggle("drawer-open", this.#drawerOpen);
			drawerToggle.classList.toggle("active", this.#drawerOpen);
			if (this.#drawerOpen) this.#renderDrawer();
		});

		this.#settingsEl = wrapper.querySelector(".settings-panel") as HTMLElement;
		const settingsToggle = wrapper.querySelector(".settings-toggle") as HTMLButtonElement;
		settingsToggle.addEventListener("click", (e) => {
			e.stopPropagation();
			this.#settingsOpen = !this.#settingsOpen;
			this.#wrapperEl!.classList.toggle("settings-open", this.#settingsOpen);
			settingsToggle.classList.toggle("active", this.#settingsOpen);
			if (this.#settingsOpen) this.#renderSettings();
			else this.#render();
		});

		const usernamePrompt = wrapper.querySelector(".username-prompt") as HTMLElement;
		const usernameInput = wrapper.querySelector(".username-input") as HTMLInputElement;
		const usernameBtn = wrapper.querySelector(".username-btn") as HTMLButtonElement;
		const addInput = wrapper.querySelector(".add-input") as HTMLInputElement;
		const addBtn = wrapper.querySelector(".add-btn") as HTMLButtonElement;

		// Show username prompt if needed
		if (!this.#userName) {
			this.#bodyEl.style.display = "none";
			usernamePrompt.style.display = "block";
		}

		const submitName = () => {
			const name = usernameInput.value.trim();
			if (name) {
				this.#setUserName(name);
				this.#bodyEl!.style.display = "flex";
				usernamePrompt.style.display = "none";
				this.#render();
			}
		};
		usernameBtn.addEventListener("click", (e) => { e.stopPropagation(); submitName(); });
		usernameInput.addEventListener("click", (e) => e.stopPropagation());
		usernameInput.addEventListener("keydown", (e) => { e.stopPropagation(); if (e.key === "Enter") submitName(); });

		// Mode tabs
		wrapper.querySelectorAll(".mode-tab").forEach((tab) => {
			tab.addEventListener("click", (e) => {
				e.stopPropagation();
				const m = (tab as HTMLElement).dataset.mode as VoteMode;
				this.#mode = m;
				wrapper.querySelectorAll(".mode-tab").forEach((t) => t.classList.remove("active"));
				tab.classList.add("active");
				this.#render();
				this.dispatchEvent(new CustomEvent("content-change"));
			});
		});

		// Add option
		const addOption = () => {
			const label = addInput.value.trim();
			if (!label) return;
			this.#options.push({
				id: `opt-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
				label,
				color: DEFAULT_COLORS[this.#options.length % DEFAULT_COLORS.length],
			});
			addInput.value = "";
			this.#render();
			this.dispatchEvent(new CustomEvent("content-change"));
		};
		addBtn.addEventListener("click", (e) => { e.stopPropagation(); addOption(); });
		addInput.addEventListener("click", (e) => e.stopPropagation());
		addInput.addEventListener("keydown", (e) => { e.stopPropagation(); if (e.key === "Enter") addOption(); });

		// Close
		wrapper.querySelector(".close-btn")!.addEventListener("click", (e) => {
			e.stopPropagation();
			this.dispatchEvent(new CustomEvent("close"));
		});

		// Title
		if (this.#title) titleEl.textContent = this.#title;

		this.#render();
		return root;
	}

	#castVote(optionId: string, delta: number) {
		if (!this.#ensureIdentity()) return;

		let vote = this.#getUserVote();
		if (!vote) {
			vote = { userId: this.#userId, userName: this.#userName, allocations: {}, timestamp: Date.now() };
			this.#votes.push(vote);
		}

		const current = vote.allocations[optionId] || 0;

		if (this.#mode === "plurality") {
			// Toggle: clear all, then set this one (or clear if already voted)
			const wasVoted = current > 0;
			for (const key of Object.keys(vote.allocations)) vote.allocations[key] = 0;
			if (!wasVoted) vote.allocations[optionId] = 1;
		} else if (this.#mode === "approval") {
			// Toggle this option
			vote.allocations[optionId] = current > 0 ? 0 : 1;
		} else {
			// Quadratic: increment/decrement
			const next = Math.max(0, current + delta);
			const testAlloc = { ...vote.allocations, [optionId]: next };
			if (quadraticCost(testAlloc) <= this.#budget) {
				vote.allocations[optionId] = next;
			}
		}

		vote.timestamp = Date.now();
		this.#render();
		this.dispatchEvent(new CustomEvent("content-change"));
	}

	#render() {
		if (this.#drawerOpen) this.#renderDrawer();
		if (!this.#optionsEl) return;

		const tally = tallyVotes(this.#votes, this.#options);
		const maxVotes = Math.max(1, ...tally.values());
		const totalVotes = [...tally.values()].reduce((a, b) => a + b, 0);
		const myVote = this.#getUserVote();
		const uniqueVoters = new Set(this.#votes.map((v) => v.userId)).size;

		this.#optionsEl.innerHTML = this.#options
			.map((opt) => {
				const count = tally.get(opt.id) || 0;
				const pct = totalVotes > 0 ? (count / totalVotes) * 100 : 0;
				const barWidth = (count / maxVotes) * 100;
				const myAlloc = myVote?.allocations[opt.id] || 0;
				const isVoted = myAlloc > 0;

				let controls = "";
				if (this.#mode === "quadratic") {
					controls = `
						<div class="qv-controls">
							<button class="qv-btn qv-minus" data-opt="${opt.id}">&minus;</button>
							<span class="qv-count">${myAlloc}</span>
							<button class="qv-btn qv-plus" data-opt="${opt.id}">+</button>
						</div>
					`;
				}

				return `
					<div class="option-row ${isVoted ? "voted" : ""}" data-opt="${opt.id}">
						<div class="bar-bg" style="width:${barWidth}%;background:${opt.color};"></div>
						<span class="option-dot" style="background:${opt.color}"></span>
						<span class="option-label">${this.#escapeHtml(opt.label)}</span>
						${controls}
						<span class="option-count">${count}</span>
						<span class="option-pct" style="color:${opt.color}">${pct.toFixed(0)}%</span>
					</div>
				`;
			})
			.join("");

		// Budget display for quadratic
		if (this.#budgetEl) {
			if (this.#mode === "quadratic") {
				const used = myVote ? quadraticCost(myVote.allocations) : 0;
				this.#budgetEl.style.display = "block";
				this.#budgetEl.innerHTML = `Credits: <span class="used">${used}</span> / ${this.#budget}`;
			} else {
				this.#budgetEl.style.display = "none";
			}
		}

		// Voter count
		if (this.#votersEl) {
			this.#votersEl.textContent = uniqueVoters === 0
				? "No votes yet"
				: `${uniqueVoters} voter${uniqueVoters !== 1 ? "s" : ""}`;
		}

		// Emit decision data on output ports
		const sorted = [...tally.entries()].sort((a, b) => b[1] - a[1]);
		const winner = sorted.length > 0 ? sorted[0] : null;
		const margin = sorted.length >= 2 && totalVotes > 0
			? (sorted[0][1] - sorted[1][1]) / totalVotes : 0;
		const decided = uniqueVoters > 0 && winner && winner[1] > 0;
		const passed = decided && margin > 0;

		this.setPortValue("results-out", Object.fromEntries(tally));
		this.setPortValue("decision-out", {
			decided,
			winner: winner ? winner[0] : null,
			passed,
			margin,
			results: Object.fromEntries(tally),
			voters: uniqueVoters,
			mode: this.#mode,
		});

		// Wire click events
		if (this.#mode !== "quadratic") {
			this.#optionsEl.querySelectorAll(".option-row").forEach((row) => {
				row.addEventListener("click", (e) => {
					e.stopPropagation();
					const optId = (row as HTMLElement).dataset.opt!;
					this.#castVote(optId, 1);
				});
			});
		} else {
			this.#optionsEl.querySelectorAll(".qv-plus").forEach((btn) => {
				btn.addEventListener("click", (e) => {
					e.stopPropagation();
					this.#castVote((btn as HTMLElement).dataset.opt!, 1);
				});
			});
			this.#optionsEl.querySelectorAll(".qv-minus").forEach((btn) => {
				btn.addEventListener("click", (e) => {
					e.stopPropagation();
					this.#castVote((btn as HTMLElement).dataset.opt!, -1);
				});
			});
		}
	}

	#renderDrawer() {
		if (!this.#drawerEl) return;

		const tally = tallyVotes(this.#votes, this.#options);
		const totalVotes = [...tally.values()].reduce((a, b) => a + b, 0);
		const maxVotes = Math.max(1, ...tally.values());
		const sorted = [...tally.entries()].sort((a, b) => b[1] - a[1]);
		const optMap = new Map(this.#options.map((o) => [o.id, o]));
		const uniqueVoters = new Set(this.#votes.map((v) => v.userId)).size;

		// Group Results
		let resultsHtml = '<div class="drawer-section"><div class="drawer-heading">Group Results</div>';
		for (const [optId, count] of sorted) {
			const opt = optMap.get(optId);
			if (!opt) continue;
			const pct = totalVotes > 0 ? (count / totalVotes) * 100 : 0;
			const barPct = (count / maxVotes) * 100;
			resultsHtml += `<div class="drawer-bar-row">
				<span class="drawer-bar-label">${this.#escapeHtml(opt.label)}</span>
				<div class="drawer-bar-bg"><div class="drawer-bar-fill" style="width:${barPct}%;background:${opt.color}"></div></div>
				<span class="drawer-bar-val">${pct.toFixed(0)}%</span>
			</div>`;
		}
		if (sorted.length >= 2) {
			const margin = totalVotes > 0 ? ((sorted[0][1] - sorted[1][1]) / totalVotes) * 100 : 0;
			resultsHtml += `<div class="stat-row"><span class="stat-label">Margin</span><span class="stat-value">${margin.toFixed(1)}%</span></div>`;
		}

		// Quadratic effective votes
		if (this.#mode === "quadratic") {
			const eff = effectiveVotes(this.#votes, this.#options);
			resultsHtml += '<div style="margin-top:6px"><div class="drawer-heading">Effective Votes (QV)</div>';
			const effSorted = [...eff.entries()].sort((a, b) => b[1] - a[1]);
			const maxEff = Math.max(1, ...eff.values());
			for (const [optId, ev] of effSorted) {
				const opt = optMap.get(optId);
				if (!opt) continue;
				resultsHtml += `<div class="drawer-bar-row">
					<span class="drawer-bar-label">${this.#escapeHtml(opt.label)}</span>
					<div class="drawer-bar-bg"><div class="drawer-bar-fill" style="width:${(ev / maxEff) * 100}%;background:${opt.color}"></div></div>
					<span class="drawer-bar-val">${ev.toFixed(1)}</span>
				</div>`;
			}
			resultsHtml += "</div>";
		}
		resultsHtml += "</div>";

		// Statistics
		const gini = giniCoefficient(tally);
		let statsHtml = '<div class="drawer-section"><div class="drawer-heading">Statistics</div>';
		statsHtml += `<div class="stat-row"><span class="stat-label">Voters</span><span class="stat-value">${uniqueVoters}</span></div>`;
		statsHtml += `<div class="stat-row"><span class="stat-label">Total votes</span><span class="stat-value">${totalVotes}</span></div>`;
		statsHtml += `<div class="stat-row"><span class="stat-label">Gini coefficient</span><span class="stat-value">${gini.toFixed(2)}</span></div>`;
		statsHtml += `<div class="stat-row"><span class="stat-label">Mode</span><span class="stat-value">${this.#mode}</span></div>`;

		if (this.#mode === "quadratic") {
			const totalCredits = this.#votes.reduce((sum, v) => sum + quadraticCost(v.allocations), 0);
			const avgCredits = uniqueVoters > 0 ? totalCredits / uniqueVoters : 0;
			statsHtml += `<div class="stat-row"><span class="stat-label">Total credits spent</span><span class="stat-value">${totalCredits}</span></div>`;
			statsHtml += `<div class="stat-row"><span class="stat-label">Avg credits/voter</span><span class="stat-value">${avgCredits.toFixed(1)}</span></div>`;
		}

		if (this.#mode === "approval") {
			for (const opt of this.#options) {
				const approvals = this.#votes.filter((v) => (v.allocations[opt.id] || 0) > 0).length;
				const rate = uniqueVoters > 0 ? (approvals / uniqueVoters) * 100 : 0;
				statsHtml += `<div class="stat-row"><span class="stat-label">${this.#escapeHtml(opt.label)} approval</span><span class="stat-value">${rate.toFixed(0)}%</span></div>`;
			}
		}
		statsHtml += "</div>";

		// Participants
		let participantsHtml = '<div class="drawer-section"><div class="drawer-heading">Participants</div>';
		for (const v of this.#votes) {
			const ago = this.#timeAgo(v.timestamp);
			const allocStr = Object.entries(v.allocations)
				.filter(([, c]) => c > 0)
				.map(([id, c]) => `${optMap.get(id)?.label || id}: ${c}`)
				.join(", ");
			participantsHtml += `<div class="participant-row">
				<span class="participant-dot" style="background:#0d9488"></span>
				<span>${this.#escapeHtml(v.userName)}</span>
				<span style="margin-left:auto;color:#94a3b8;font-size:10px">${allocStr || "none"} (${ago})</span>
			</div>`;
		}
		participantsHtml += "</div>";

		this.#drawerEl.innerHTML = resultsHtml + statsHtml + participantsHtml;
	}

	#timeAgo(ts: number): string {
		const diff = Date.now() - ts;
		if (diff < 60000) return "just now";
		if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;
		if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;
		return `${Math.floor(diff / 86400000)}d ago`;
	}

	#escapeHtml(text: string): string {
		const div = document.createElement("div");
		div.textContent = text;
		return div.innerHTML;
	}

	#renderSettings() {
		if (!this.#settingsEl) return;
		const esc = (s: string) => this.#escapeHtml(s);
		let h = '<div><div class="settings-label">Title</div>';
		h += `<input type="text" class="settings-input settings-title" value="${esc(this.#title)}" /></div>`;
		h += '<div><div class="settings-label">Options</div>';
		for (let i = 0; i < this.#options.length; i++) {
			const opt = this.#options[i];
			h += `<div class="settings-item" data-idx="${i}">`;
			h += `<input type="color" class="opt-color" value="${opt.color}" />`;
			h += `<input type="text" class="opt-label" value="${esc(opt.label)}" />`;
			h += `<button class="remove-btn" title="Remove">&times;</button></div>`;
		}
		h += '</div>';
		h += '<div><div class="settings-label">Quadratic Budget</div>';
		h += `<div class="settings-item"><input type="number" class="settings-budget" value="${this.#budget}" min="1" max="10000" />`;
		h += '<span style="font-size:11px;color:#64748b">credits per voter</span></div></div>';
		h += '<div><div class="settings-label">Danger Zone</div>';
		h += `<button class="settings-danger clear-data-btn">Reset all votes (${this.#votes.length})</button></div>`;
		h += '<button class="settings-done">Done</button>';
		this.#settingsEl.innerHTML = h;

		const stop = (e: Event) => e.stopPropagation();
		const titleInput = this.#settingsEl.querySelector(".settings-title") as HTMLInputElement;
		titleInput.addEventListener("click", stop);
		titleInput.addEventListener("input", () => {
			this.#title = titleInput.value;
			this.#wrapperEl!.querySelector(".title-text")!.textContent = this.#title;
			this.dispatchEvent(new CustomEvent("content-change"));
		});
		this.#settingsEl.querySelectorAll(".opt-label").forEach((el) => {
			const input = el as HTMLInputElement;
			input.addEventListener("click", stop);
			input.addEventListener("input", () => {
				const idx = parseInt(input.closest(".settings-item")!.getAttribute("data-idx")!);
				this.#options[idx].label = input.value;
				this.dispatchEvent(new CustomEvent("content-change"));
			});
		});
		this.#settingsEl.querySelectorAll(".opt-color").forEach((el) => {
			const input = el as HTMLInputElement;
			input.addEventListener("click", stop);
			input.addEventListener("input", () => {
				const idx = parseInt(input.closest(".settings-item")!.getAttribute("data-idx")!);
				this.#options[idx].color = input.value;
				this.dispatchEvent(new CustomEvent("content-change"));
			});
		});
		this.#settingsEl.querySelectorAll(".remove-btn").forEach((btn) => {
			btn.addEventListener("click", (e) => {
				e.stopPropagation();
				const idx = parseInt((btn as HTMLElement).closest(".settings-item")!.getAttribute("data-idx")!);
				const removedId = this.#options[idx].id;
				this.#options.splice(idx, 1);
				for (const v of this.#votes) delete v.allocations[removedId];
				this.#renderSettings();
				this.dispatchEvent(new CustomEvent("content-change"));
			});
		});
		const budgetInput = this.#settingsEl.querySelector(".settings-budget") as HTMLInputElement;
		if (budgetInput) {
			budgetInput.addEventListener("click", stop);
			budgetInput.addEventListener("input", () => {
				this.#budget = parseInt(budgetInput.value) || 100;
				this.dispatchEvent(new CustomEvent("content-change"));
			});
		}
		this.#settingsEl.querySelector(".clear-data-btn")!.addEventListener("click", (e) => {
			e.stopPropagation();
			this.#votes = [];
			this.#renderSettings();
			this.dispatchEvent(new CustomEvent("content-change"));
		});
		this.#settingsEl.querySelector(".settings-done")!.addEventListener("click", (e) => {
			e.stopPropagation();
			this.#settingsOpen = false;
			this.#wrapperEl!.classList.remove("settings-open");
			this.#wrapperEl!.querySelector(".settings-toggle")!.classList.remove("active");
			this.#render();
		});
	}

	static override fromData(data: Record<string, any>): FolkChoiceVote {
		const el = FolkShape.fromData(data) as FolkChoiceVote;
		if (data.title !== undefined) el.title = data.title;
		if (data.options !== undefined) el.options = data.options;
		if (data.mode !== undefined) el.mode = data.mode;
		if (data.budget != null) el.budget = data.budget;
		if (data.votes !== undefined) el.votes = data.votes;
		return el;
	}

	override toJSON() {
		return {
			...super.toJSON(),
			type: "folk-choice-vote",
			title: this.#title,
			options: this.#options,
			mode: this.#mode,
			budget: this.#budget,
			votes: this.#votes,
		};
	}

	override applyData(data: Record<string, any>): void {
		super.applyData(data);
		if (data.title !== undefined && this.#title !== data.title) this.title = data.title;
		if (data.options !== undefined && JSON.stringify(this.#options) !== JSON.stringify(data.options)) this.options = data.options;
		if (data.mode !== undefined && this.#mode !== data.mode) this.mode = data.mode;
		if (data.budget != null && this.#budget !== data.budget) this.budget = data.budget;
		if (data.votes !== undefined && JSON.stringify(this.#votes) !== JSON.stringify(data.votes)) this.votes = data.votes;
	}
}
