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: 340px;
		min-height: 380px;
	}

	.header {
		display: flex;
		align-items: center;
		justify-content: space-between;
		padding: 8px 12px;
		background: #4f46e5;
		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;
	}

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

	.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;
	}

	.tab.active {
		color: #4f46e5;
		border-bottom: 2px solid #4f46e5;
		background: rgba(79, 70, 229, 0.1);
	}

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

	.panel.hidden { display: none; }

	/* Rank list */
	.rank-item {
		display: flex;
		align-items: center;
		gap: 8px;
		padding: 8px;
		border-radius: 6px;
		margin-bottom: 4px;
		background: var(--rs-bg-surface-sunken, #0f172a);
		border: 1px solid var(--rs-border, rgba(255, 255, 255, 0.1));
		cursor: grab;
		user-select: none;
		transition: transform 0.15s, box-shadow 0.15s;
	}

	.rank-item:active { cursor: grabbing; }

	.rank-item.dragging {
		opacity: 0.5;
		background: rgba(79, 70, 229, 0.1);
	}

	.rank-item.drag-over {
		border-color: #4f46e5;
		transform: scale(1.02);
		box-shadow: 0 2px 8px rgba(79, 70, 229, 0.15);
	}

	.grip {
		color: var(--rs-text-secondary, #94a3b8);
		font-size: 10px;
		line-height: 1;
		flex-shrink: 0;
	}

	.rank-num {
		font-size: 13px;
		font-weight: 700;
		min-width: 18px;
		text-align: center;
		flex-shrink: 0;
	}

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

	/* Results */
	.results-section {
		margin-bottom: 12px;
	}

	.results-heading {
		font-size: 11px;
		font-weight: 600;
		color: var(--rs-text-muted, #64748b);
		text-transform: uppercase;
		letter-spacing: 0.5px;
		margin-bottom: 6px;
	}

	.borda-row {
		display: flex;
		align-items: center;
		gap: 8px;
		margin-bottom: 4px;
	}

	.borda-label {
		font-size: 12px;
		min-width: 80px;
		color: var(--rs-text-primary, #e2e8f0);
	}

	.borda-bar-bg {
		flex: 1;
		height: 14px;
		background: var(--rs-bg-surface-raised, #334155);
		border-radius: 3px;
		overflow: hidden;
	}

	.borda-bar-fill {
		height: 100%;
		background: #4f46e5;
		border-radius: 3px;
		transition: width 0.4s ease;
	}

	.borda-pts {
		font-size: 11px;
		font-weight: 600;
		color: #4f46e5;
		min-width: 24px;
		text-align: right;
		font-variant-numeric: tabular-nums;
	}

	.irv-round {
		font-size: 11px;
		padding: 4px 0;
		border-bottom: 1px solid var(--rs-border-subtle, rgba(255, 255, 255, 0.06));
		color: var(--rs-text-secondary, #94a3b8);
	}

	.irv-round .round-num {
		font-weight: 600;
		color: #4f46e5;
	}

	.irv-round .eliminated {
		color: #ef4444;
		font-weight: 500;
	}

	.irv-round .winner {
		color: #22c55e;
		font-weight: 600;
	}

	.voters-count {
		padding: 4px 0;
		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 #e2e8f0;
	}

	.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: #4f46e5; }

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

	.add-btn:hover { background: #4338ca; }

	.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: #4f46e5;
		color: white;
		border: none;
		border-radius: 6px;
		padding: 8px 16px;
		cursor: pointer;
		font-weight: 500;
		font-size: 13px;
	}

	.empty-state {
		text-align: center;
		padding: 24px 12px;
		color: #94a3b8;
		font-size: 12px;
	}

	.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: #4f46e5; }
	.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 .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: #4f46e5; 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: #4338ca; }
`;

// -- Data types --

export interface RankOption {
	id: string;
	label: string;
}

export interface UserRanking {
	userId: string;
	userName: string;
	ordering: string[];
	timestamp: number;
}

export interface IRVRound {
	round: number;
	counts: Record<string, number>;
	eliminated: string | null;
}

// -- Pure aggregation functions --

export function bordaCount(
	rankings: UserRanking[],
	options: RankOption[],
): Map<string, number> {
	const n = options.length;
	const scores = new Map<string, number>();
	for (const opt of options) scores.set(opt.id, 0);
	for (const r of rankings) {
		for (let i = 0; i < r.ordering.length; i++) {
			const points = n - 1 - i;
			const optId = r.ordering[i];
			if (scores.has(optId)) {
				scores.set(optId, scores.get(optId)! + points);
			}
		}
	}
	return scores;
}

export function instantRunoff(
	rankings: UserRanking[],
	options: RankOption[],
): IRVRound[] {
	if (rankings.length === 0 || options.length === 0) return [];

	const rounds: IRVRound[] = [];
	const remaining = new Set(options.map((o) => o.id));
	let round = 1;

	while (remaining.size > 1) {
		// Count first-place votes among remaining options
		const counts: Record<string, number> = {};
		for (const id of remaining) counts[id] = 0;

		for (const r of rankings) {
			const top = r.ordering.find((id) => remaining.has(id));
			if (top) counts[top]++;
		}

		const totalVotes = rankings.length;

		// Check for majority
		let majorityWinner: string | null = null;
		for (const [id, count] of Object.entries(counts)) {
			if (count > totalVotes / 2) {
				majorityWinner = id;
				break;
			}
		}

		if (majorityWinner) {
			rounds.push({ round, counts, eliminated: null });
			break;
		}

		// Find minimum vote count
		let minCount = Infinity;
		for (const count of Object.values(counts)) {
			if (count < minCount) minCount = count;
		}

		// Eliminate first option with minimum count
		let eliminated: string | null = null;
		for (const [id, count] of Object.entries(counts)) {
			if (count === minCount) {
				eliminated = id;
				break;
			}
		}

		rounds.push({ round, counts, eliminated });
		if (eliminated) remaining.delete(eliminated);
		round++;
	}

	// If eliminated down to 1
	if (remaining.size === 1 && (rounds.length === 0 || rounds[rounds.length - 1].eliminated !== null)) {
		const lastId = [...remaining][0];
		rounds.push({ round, counts: { [lastId]: rankings.length }, eliminated: null });
	}

	return rounds;
}

export function kendallTauB(rankings: UserRanking[], options: RankOption[]): number {
	if (rankings.length < 2) return 0;
	const optIds = options.map((o) => o.id);
	let totalTau = 0;
	let pairs = 0;
	for (let i = 0; i < rankings.length; i++) {
		for (let j = i + 1; j < rankings.length; j++) {
			const posA = new Map(rankings[i].ordering.map((id, idx) => [id, idx]));
			const posB = new Map(rankings[j].ordering.map((id, idx) => [id, idx]));
			let concordant = 0;
			let discordant = 0;
			for (let a = 0; a < optIds.length; a++) {
				for (let b = a + 1; b < optIds.length; b++) {
					const dA = (posA.get(optIds[a]) ?? 0) - (posA.get(optIds[b]) ?? 0);
					const dB = (posB.get(optIds[a]) ?? 0) - (posB.get(optIds[b]) ?? 0);
					if (dA * dB > 0) concordant++;
					else if (dA * dB < 0) discordant++;
				}
			}
			const total = concordant + discordant;
			if (total > 0) {
				totalTau += (concordant - discordant) / total;
				pairs++;
			}
		}
	}
	return pairs > 0 ? totalTau / pairs : 0;
}

export function positionFrequency(
	rankings: UserRanking[],
	options: RankOption[],
): Map<string, number[]> {
	const result = new Map<string, number[]>();
	const n = options.length;
	for (const opt of options) result.set(opt.id, new Array(n).fill(0));
	for (const r of rankings) {
		for (let i = 0; i < r.ordering.length; i++) {
			const counts = result.get(r.ordering[i]);
			if (counts && i < counts.length) counts[i]++;
		}
	}
	return result;
}

export function headToHead(
	rankings: UserRanking[],
	options: RankOption[],
): Map<string, Map<string, number>> {
	const result = new Map<string, Map<string, number>>();
	for (const a of options) {
		result.set(a.id, new Map());
		for (const b of options) {
			if (a.id !== b.id) result.get(a.id)!.set(b.id, 0);
		}
	}
	for (const r of rankings) {
		const pos = new Map(r.ordering.map((id, idx) => [id, idx]));
		for (const a of options) {
			for (const b of options) {
				if (a.id === b.id) continue;
				if ((pos.get(a.id) ?? Infinity) < (pos.get(b.id) ?? Infinity)) {
					result.get(a.id)!.set(b.id, result.get(a.id)!.get(b.id)! + 1);
				}
			}
		}
	}
	return result;
}

export function condorcetWinner(rankings: UserRanking[], options: RankOption[]): string | null {
	const h2h = headToHead(rankings, options);
	for (const a of options) {
		let wins = true;
		for (const b of options) {
			if (a.id === b.id) continue;
			if ((h2h.get(a.id)?.get(b.id) ?? 0) <= (h2h.get(b.id)?.get(a.id) ?? 0)) {
				wins = false;
				break;
			}
		}
		if (wins) return a.id;
	}
	return null;
}

// -- Component --

declare global {
	interface HTMLElementTagNameMap {
		"folk-choice-rank": FolkChoiceRank;
	}
}

const MEDAL_COLORS = ["#f59e0b", "#94a3b8", "#cd7f32", "#64748b", "#64748b"];

export class FolkChoiceRank extends FolkShape {
	static override tagName = "folk-choice-rank";

	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 = "Rank Choices";
	#options: RankOption[] = [];
	#rankings: UserRanking[] = [];
	#userId = "";
	#userName = "";
	#activeTab: "rank" | "results" = "rank";
	#drawerOpen = false;
	#settingsOpen = false;

	// Drag state
	#dragIdx: number | null = null;
	#myOrdering: string[] = [];

	// DOM refs
	#wrapperEl: HTMLElement | null = null;
	#bodyEl: HTMLElement | null = null;
	#rankPanel: HTMLElement | null = null;
	#resultsPanel: 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: RankOption[]) {
		this.#options = v;
		this.#syncMyOrdering();
		this.#render();
		this.requestUpdate("options");
	}

	get rankings() { return this.#rankings; }
	set rankings(v: UserRanking[]) {
		this.#rankings = v;
		this.#syncMyOrdering();
		this.#render();
		this.requestUpdate("rankings");
	}

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

	#syncMyOrdering() {
		const mine = this.#rankings.find((r) => r.userId === this.#userId);
		if (mine) {
			// Keep existing ordering, add new options at end
			const existing = new Set(mine.ordering);
			const extra = this.#options.filter((o) => !existing.has(o.id)).map((o) => o.id);
			this.#myOrdering = [...mine.ordering.filter((id) => this.#options.some((o) => o.id === id)), ...extra];
		} else {
			this.#myOrdering = this.#options.map((o) => o.id);
		}
	}

	#saveMyRanking() {
		if (!this.#ensureIdentity()) return;
		const idx = this.#rankings.findIndex((r) => r.userId === this.#userId);
		const entry: UserRanking = {
			userId: this.#userId,
			userName: this.#userName,
			ordering: [...this.#myOrdering],
			timestamp: Date.now(),
		};
		if (idx >= 0) {
			this.#rankings[idx] = entry;
		} else {
			this.#rankings.push(entry);
		}
		this.dispatchEvent(new CustomEvent("content-change"));
	}

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

		const wrapper = document.createElement("div");
		wrapper.className = "wrapper";
		wrapper.innerHTML = html`
			<div class="header">
				<span class="header-title">
					<span>&#x1F4CA;</span>
					<span class="title-text">Rank</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="tab-bar">
					<button class="tab active" data-tab="rank">My Ranking</button>
					<button class="tab" data-tab="results">Results</button>
				</div>
				<div class="panel rank-panel"></div>
				<div class="panel results-panel hidden"></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 rank:</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.#rankPanel = wrapper.querySelector(".rank-panel") as HTMLElement;
		this.#resultsPanel = wrapper.querySelector(".results-panel") as HTMLElement;
		this.#drawerEl = wrapper.querySelector(".results-drawer") 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;

		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.#saveMyRanking();
				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(); });

		// Tabs
		wrapper.querySelectorAll(".tab").forEach((tab) => {
			tab.addEventListener("click", (e) => {
				e.stopPropagation();
				const t = (tab as HTMLElement).dataset.tab as "rank" | "results";
				this.#activeTab = t;
				wrapper.querySelectorAll(".tab").forEach((tb) => tb.classList.remove("active"));
				tab.classList.add("active");
				this.#rankPanel!.classList.toggle("hidden", t !== "rank");
				this.#resultsPanel!.classList.toggle("hidden", t !== "results");
				this.#render();
			});
		});

		// Add option
		const addOption = () => {
			const label = addInput.value.trim();
			if (!label) return;
			const id = `opt-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
			this.#options.push({ id, label });
			this.#myOrdering.push(id);
			addInput.value = "";
			this.#saveMyRanking();
			this.#render();
		};
		addBtn.addEventListener("click", (e) => { e.stopPropagation(); addOption(); });
		addInput.addEventListener("click", (e) => e.stopPropagation());
		addInput.addEventListener("keydown", (e) => { e.stopPropagation(); if (e.key === "Enter") addOption(); });

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

		this.#render();
		return root;
	}

	#render() {
		if (this.#activeTab === "rank") this.#renderRankList();
		else this.#renderResults();
		if (this.#drawerOpen) this.#renderDrawer();

		// Emit decision data on output ports
		if (this.#rankings.length > 0 && this.#options.length > 0) {
			const borda = bordaCount(this.#rankings, this.#options);
			const sorted = [...borda.entries()].sort((a, b) => b[1] - a[1]);
			const winner = sorted[0]?.[0] ?? null;
			const cw = condorcetWinner(this.#rankings, this.#options);
			const uniqueVoters = new Set(this.#rankings.map(r => r.userId)).size;

			this.setPortValue("results-out", Object.fromEntries(borda));
			this.setPortValue("decision-out", {
				decided: uniqueVoters > 0,
				winner,
				condorcetWinner: cw,
				passed: !!winner,
				results: Object.fromEntries(borda),
				voters: uniqueVoters,
			});
		}
	}

	#renderRankList() {
		if (!this.#rankPanel) return;

		if (this.#myOrdering.length === 0) {
			this.#rankPanel.innerHTML = '<div class="empty-state">Add options below to start ranking</div>';
			return;
		}

		const optMap = new Map(this.#options.map((o) => [o.id, o]));

		this.#rankPanel.innerHTML = this.#myOrdering
			.map((id, i) => {
				const opt = optMap.get(id);
				if (!opt) return "";
				const color = MEDAL_COLORS[Math.min(i, MEDAL_COLORS.length - 1)];
				return `
					<div class="rank-item" data-idx="${i}">
						<span class="grip">&#x2261;&#x2261;</span>
						<span class="rank-num" style="color:${color}">${i + 1}</span>
						<span class="rank-label">${this.#escapeHtml(opt.label)}</span>
					</div>
				`;
			})
			.join("");

		// Wire pointer-based drag
		this.#rankPanel.querySelectorAll(".rank-item").forEach((item) => {
			const el = item as HTMLElement;
			el.addEventListener("pointerdown", (e) => {
				e.stopPropagation();
				this.#dragIdx = parseInt(el.dataset.idx!);
				el.classList.add("dragging");
				el.setPointerCapture(e.pointerId);

				const onMove = (me: PointerEvent) => {
					me.stopPropagation();
					this.#rankPanel!.querySelectorAll(".drag-over").forEach((d) => d.classList.remove("drag-over"));
					const items = this.#rankPanel!.querySelectorAll(".rank-item:not(.dragging)");
					for (const item of items) {
						const rect = (item as HTMLElement).getBoundingClientRect();
						if (me.clientY >= rect.top && me.clientY <= rect.bottom) {
							item.classList.add("drag-over");
							break;
						}
					}
				};

				const onUp = (ue: PointerEvent) => {
					ue.stopPropagation();
					el.classList.remove("dragging");
					el.releasePointerCapture(ue.pointerId);
					el.removeEventListener("pointermove", onMove);
					el.removeEventListener("pointerup", onUp);

					// Find drop target
					const overEl = this.#rankPanel!.querySelector(".drag-over") as HTMLElement;
					if (overEl && this.#dragIdx !== null) {
						const targetIdx = parseInt(overEl.dataset.idx!);
						if (targetIdx !== this.#dragIdx) {
							const [moved] = this.#myOrdering.splice(this.#dragIdx, 1);
							this.#myOrdering.splice(targetIdx, 0, moved);
							this.#saveMyRanking();
						}
					}

					this.#rankPanel!.querySelectorAll(".drag-over").forEach((d) => d.classList.remove("drag-over"));
					this.#dragIdx = null;
					this.#renderRankList();
				};

				el.addEventListener("pointermove", onMove);
				el.addEventListener("pointerup", onUp);
			});

			// Keyboard reorder
			el.tabIndex = 0;
			el.addEventListener("keydown", (e) => {
				e.stopPropagation();
				const idx = parseInt(el.dataset.idx!);
				if (e.key === "ArrowUp" && idx > 0) {
					[this.#myOrdering[idx - 1], this.#myOrdering[idx]] = [this.#myOrdering[idx], this.#myOrdering[idx - 1]];
					this.#saveMyRanking();
					this.#renderRankList();
					(this.#rankPanel!.querySelector(`[data-idx="${idx - 1}"]`) as HTMLElement)?.focus();
				} else if (e.key === "ArrowDown" && idx < this.#myOrdering.length - 1) {
					[this.#myOrdering[idx], this.#myOrdering[idx + 1]] = [this.#myOrdering[idx + 1], this.#myOrdering[idx]];
					this.#saveMyRanking();
					this.#renderRankList();
					(this.#rankPanel!.querySelector(`[data-idx="${idx + 1}"]`) as HTMLElement)?.focus();
				}
			});
		});
	}

	#renderResults() {
		if (!this.#resultsPanel) return;

		const uniqueVoters = new Set(this.#rankings.map((r) => r.userId)).size;

		if (this.#rankings.length === 0) {
			this.#resultsPanel.innerHTML = '<div class="empty-state">No rankings submitted yet</div>';
			return;
		}

		const optMap = new Map(this.#options.map((o) => [o.id, o]));

		// Borda count
		const borda = bordaCount(this.#rankings, this.#options);
		const maxBorda = Math.max(1, ...borda.values());
		const bordaSorted = [...borda.entries()].sort((a, b) => b[1] - a[1]);

		let bordaHtml = '<div class="results-section"><div class="results-heading">Borda Count</div>';
		for (const [optId, pts] of bordaSorted) {
			const opt = optMap.get(optId);
			if (!opt) continue;
			const pct = (pts / maxBorda) * 100;
			bordaHtml += `
				<div class="borda-row">
					<span class="borda-label">${this.#escapeHtml(opt.label)}</span>
					<div class="borda-bar-bg"><div class="borda-bar-fill" style="width:${pct}%"></div></div>
					<span class="borda-pts">${pts}</span>
				</div>
			`;
		}
		bordaHtml += "</div>";

		// IRV
		const irvRounds = instantRunoff(this.#rankings, this.#options);
		let irvHtml = '<div class="results-section"><div class="results-heading">Instant-Runoff</div>';
		for (const r of irvRounds) {
			const counts = Object.entries(r.counts)
				.map(([id, c]) => `${optMap.get(id)?.label || id}: ${c}`)
				.join(", ");
			if (r.eliminated) {
				const elimLabel = optMap.get(r.eliminated)?.label || r.eliminated;
				irvHtml += `<div class="irv-round"><span class="round-num">Round ${r.round}:</span> ${counts} &mdash; <span class="eliminated">${this.#escapeHtml(elimLabel)} eliminated</span></div>`;
			} else {
				// Winner round
				const winnerId = Object.keys(r.counts)[0];
				const winLabel = optMap.get(winnerId)?.label || winnerId;
				irvHtml += `<div class="irv-round"><span class="round-num">Round ${r.round}:</span> ${counts} &mdash; <span class="winner">${this.#escapeHtml(winLabel)} wins!</span></div>`;
			}
		}
		irvHtml += "</div>";

		this.#resultsPanel.innerHTML = bordaHtml + irvHtml +
			`<div class="voters-count">${uniqueVoters} voter${uniqueVoters !== 1 ? "s" : ""}</div>`;
	}

	#renderDrawer() {
		if (!this.#drawerEl) return;
		const optMap = new Map(this.#options.map((o) => [o.id, o]));

		// Borda count
		const borda = bordaCount(this.#rankings, this.#options);
		const maxBorda = Math.max(1, ...borda.values());
		const bordaSorted = [...borda.entries()].sort((a, b) => b[1] - a[1]);

		let resultsHtml = '<div class="drawer-section"><div class="drawer-heading">Group Results</div>';
		for (const [optId, pts] of bordaSorted) {
			const opt = optMap.get(optId);
			if (!opt) continue;
			const pct = (pts / maxBorda) * 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:${pct}%;background:#4f46e5"></div></div>
				<span class="drawer-bar-val">${pts}</span>
			</div>`;
		}

		// IRV winner
		const irvRounds = instantRunoff(this.#rankings, this.#options);
		if (irvRounds.length > 0) {
			const lastRound = irvRounds[irvRounds.length - 1];
			const winnerId = Object.keys(lastRound.counts)[0];
			const winLabel = optMap.get(winnerId)?.label || winnerId;
			resultsHtml += `<div class="stat-row"><span class="stat-label">IRV Winner</span><span class="stat-value">${this.#escapeHtml(winLabel)}</span></div>`;
		}

		// Condorcet winner
		const cw = condorcetWinner(this.#rankings, this.#options);
		if (cw) {
			const cwLabel = optMap.get(cw)?.label || cw;
			resultsHtml += `<div class="stat-row"><span class="stat-label">Condorcet Winner</span><span class="stat-value" style="color:#22c55e">${this.#escapeHtml(cwLabel)}</span></div>`;
		} else {
			resultsHtml += `<div class="stat-row"><span class="stat-label">Condorcet Winner</span><span class="stat-value" style="color:#94a3b8">None</span></div>`;
		}
		resultsHtml += "</div>";

		// Statistics
		const uniqueVoters = new Set(this.#rankings.map((r) => r.userId)).size;
		const tau = kendallTauB(this.#rankings, this.#options);
		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">Agreement (Kendall)</span><span class="stat-value">${tau.toFixed(2)}</span></div>`;

		// Head-to-head
		const h2h = headToHead(this.#rankings, this.#options);
		for (const a of this.#options) {
			for (const b of this.#options) {
				if (a.id >= b.id) continue;
				const aWins = h2h.get(a.id)?.get(b.id) ?? 0;
				const bWins = h2h.get(b.id)?.get(a.id) ?? 0;
				statsHtml += `<div class="stat-row"><span class="stat-label">${this.#escapeHtml(a.label)} v ${this.#escapeHtml(b.label)}</span><span class="stat-value">${aWins}-${bWins}</span></div>`;
			}
		}
		statsHtml += "</div>";

		// Participants
		let participantsHtml = '<div class="drawer-section"><div class="drawer-heading">Participants</div>';
		for (const r of this.#rankings) {
			const ago = this.#timeAgo(r.timestamp);
			participantsHtml += `<div class="participant-row">
				<span class="participant-dot" style="background:#4f46e5"></span>
				<span>${this.#escapeHtml(r.userName)}</span>
				<span style="margin-left:auto;color:#94a3b8">${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="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">Danger Zone</div>';
		h += `<button class="settings-danger clear-data-btn">Reset all rankings (${this.#rankings.length} voter${this.#rankings.length !== 1 ? "s" : ""})</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(".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 r of this.#rankings) {
					r.ordering = r.ordering.filter((id) => id !== removedId);
				}
				this.#myOrdering = this.#myOrdering.filter((id) => id !== removedId);
				this.#renderSettings();
				this.dispatchEvent(new CustomEvent("content-change"));
			});
		});
		this.#settingsEl.querySelector(".clear-data-btn")!.addEventListener("click", (e) => {
			e.stopPropagation();
			this.#rankings = [];
			this.#myOrdering = this.#options.map((o) => o.id);
			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.#syncMyOrdering();
			this.#render();
		});
	}

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

	override toJSON() {
		return {
			...super.toJSON(),
			type: "folk-choice-rank",
			title: this.#title,
			options: this.#options,
			rankings: this.#rankings,
		};
	}

	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.rankings !== undefined && JSON.stringify(this.#rankings) !== JSON.stringify(data.rankings)) this.rankings = data.rankings;
	}
}
