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: 400px;
		min-height: 480px;
	}

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

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

	.option-tab {
		padding: 6px 12px;
		text-align: center;
		font-size: 11px;
		font-weight: 500;
		cursor: pointer;
		border: none;
		background: transparent;
		color: var(--rs-text-muted, #64748b);
		white-space: nowrap;
		transition: all 0.15s;
		flex-shrink: 0;
	}

	.option-tab.active {
		color: #059669;
		border-bottom: 2px solid #059669;
		background: rgba(5, 150, 105, 0.1);
	}

	.chart-area {
		display: flex;
		justify-content: center;
		padding: 8px;
		flex-shrink: 0;
	}

	.chart-area svg {
		max-width: 260px;
		max-height: 240px;
	}

	.sliders {
		padding: 4px 12px;
		overflow-y: auto;
		flex: 1;
	}

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

	.slider-label {
		font-size: 11px;
		color: var(--rs-text-muted, #64748b);
		min-width: 60px;
		text-align: right;
	}

	.slider-input {
		flex: 1;
		height: 4px;
		-webkit-appearance: none;
		appearance: none;
		background: var(--rs-bg-surface-raised, #334155);
		border-radius: 2px;
		outline: none;
		cursor: pointer;
	}

	.slider-input::-webkit-slider-thumb {
		-webkit-appearance: none;
		width: 14px;
		height: 14px;
		border-radius: 50%;
		background: #059669;
		cursor: pointer;
	}
	.slider-input::-moz-range-thumb {
		width: 14px;
		height: 14px;
		border: none;
		border-radius: 50%;
		background: #059669;
		cursor: pointer;
	}
	.slider-input::-moz-range-track {
		background: #e2e8f0;
		border-radius: 2px;
		height: 4px;
	}

	.slider-val {
		font-size: 12px;
		font-weight: 600;
		color: #059669;
		min-width: 18px;
		text-align: center;
		font-variant-numeric: tabular-nums;
	}

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

	.legend-item {
		display: flex;
		align-items: center;
		gap: 4px;
		font-size: 10px;
		color: #64748b;
	}

	.legend-dot {
		width: 8px;
		height: 8px;
		border-radius: 50%;
	}

	.score-summary {
		padding: 4px 12px;
		font-size: 11px;
		color: #64748b;
		text-align: center;
		border-top: 1px solid #e2e8f0;
	}

	.score-summary .best {
		font-weight: 600;
		color: #059669;
	}

	.add-forms {
		padding: 6px 12px;
		border-top: 1px solid #e2e8f0;
		display: flex;
		gap: 6px;
	}

	.add-forms .add-group {
		flex: 1;
		display: flex;
		gap: 4px;
	}

	.add-forms input {
		flex: 1;
		min-width: 0;
		border: 1px solid #e2e8f0;
		border-radius: 4px;
		padding: 4px 6px;
		font-size: 11px;
		outline: none;
	}

	.add-forms input:focus { border-color: #059669; }

	.add-forms button {
		background: #059669;
		color: white;
		border: none;
		border-radius: 4px;
		padding: 4px 8px;
		cursor: pointer;
		font-size: 10px;
		font-weight: 500;
		white-space: nowrap;
	}

	.add-forms button:hover { background: #047857; }

	.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: #059669;
		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: #059669; }
	.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="range"] { width: 60px; height: 4px; -webkit-appearance: none; appearance: none; background: #e2e8f0; border-radius: 2px; outline: none; cursor: pointer; }
	.settings-item input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; border-radius: 50%; background: #059669; cursor: pointer; }
	.settings-item input[type="range"]::-moz-range-thumb { width: 12px; height: 12px; border: none; border-radius: 50%; background: #059669; cursor: pointer; }
	.settings-item input[type="range"]::-moz-range-track { background: #e2e8f0; border-radius: 2px; height: 4px; }
	.settings-item .weight-val { font-size: 10px; font-weight: 600; color: #059669; min-width: 12px; text-align: center; }
	.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: #059669; 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: #047857; }
`;

// -- Data types --

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

export interface SpiderCriterion {
	id: string;
	label: string;
	weight: number;
}

export interface SpiderScore {
	userId: string;
	userName: string;
	optionId: string;
	criterionId: string;
	value: number;
	timestamp: number;
}

// -- Pure aggregation functions --

export function weightedMeanScore(
	scores: SpiderScore[],
	criteria: SpiderCriterion[],
	optionId: string,
): number {
	const byCriterion = new Map<string, number[]>();
	for (const s of scores) {
		if (s.optionId !== optionId) continue;
		if (!byCriterion.has(s.criterionId)) byCriterion.set(s.criterionId, []);
		byCriterion.get(s.criterionId)!.push(s.value);
	}

	let weightedSum = 0;
	let totalWeight = 0;
	for (const c of criteria) {
		const vals = byCriterion.get(c.id);
		if (!vals || vals.length === 0) continue;
		const avg = vals.reduce((a, b) => a + b, 0) / vals.length;
		weightedSum += avg * c.weight;
		totalWeight += c.weight;
	}
	return totalWeight > 0 ? weightedSum / totalWeight : 0;
}

export function getRadarVertices(
	scores: SpiderScore[],
	criteria: SpiderCriterion[],
	optionId: string,
	userId: string,
	cx: number,
	cy: number,
	radius: number,
): { x: number; y: number }[] {
	const n = criteria.length;
	if (n === 0) return [];
	const angleStep = (2 * Math.PI) / n;

	return criteria.map((c, i) => {
		const score = scores.find(
			(s) => s.optionId === optionId && s.criterionId === c.id && s.userId === userId,
		);
		const val = score ? score.value / 10 : 0;
		const angle = i * angleStep - Math.PI / 2;
		return {
			x: cx + radius * val * Math.cos(angle),
			y: cy + radius * val * Math.sin(angle),
		};
	});
}

export function getAverageRadarVertices(
	scores: SpiderScore[],
	criteria: SpiderCriterion[],
	optionId: string,
	cx: number,
	cy: number,
	radius: number,
): { x: number; y: number }[] {
	const n = criteria.length;
	if (n === 0) return [];
	const angleStep = (2 * Math.PI) / n;

	const byCriterion = new Map<string, number[]>();
	for (const s of scores) {
		if (s.optionId !== optionId) continue;
		if (!byCriterion.has(s.criterionId)) byCriterion.set(s.criterionId, []);
		byCriterion.get(s.criterionId)!.push(s.value);
	}

	return criteria.map((c, i) => {
		const vals = byCriterion.get(c.id) || [];
		const avg = vals.length > 0 ? vals.reduce((a, b) => a + b, 0) / vals.length : 0;
		const val = avg / 10;
		const angle = i * angleStep - Math.PI / 2;
		return {
			x: cx + radius * val * Math.cos(angle),
			y: cy + radius * val * Math.sin(angle),
		};
	});
}

export function polygonArea(vertices: { x: number; y: number }[]): number {
	const n = vertices.length;
	if (n < 3) return 0;
	let area = 0;
	for (let i = 0; i < n; i++) {
		const j = (i + 1) % n;
		area += vertices[i].x * vertices[j].y;
		area -= vertices[j].x * vertices[i].y;
	}
	return Math.abs(area) / 2;
}

export function criterionStats(
	scores: SpiderScore[],
	criterionId: string,
	optionId: string,
): { mean: number; stdDev: number; min: number; max: number; count: number } {
	const vals = scores
		.filter((s) => s.criterionId === criterionId && s.optionId === optionId)
		.map((s) => s.value);
	if (vals.length === 0) return { mean: 0, stdDev: 0, min: 0, max: 0, count: 0 };
	const mean = vals.reduce((a, b) => a + b, 0) / vals.length;
	const variance = vals.reduce((sum, v) => sum + (v - mean) ** 2, 0) / vals.length;
	return {
		mean,
		stdDev: Math.sqrt(variance),
		min: Math.min(...vals),
		max: Math.max(...vals),
		count: vals.length,
	};
}

export function consensusIndex(
	scores: SpiderScore[],
	criteria: SpiderCriterion[],
	optionId: string,
): number {
	if (criteria.length === 0) return 0;
	let totalStdDev = 0;
	let counted = 0;
	for (const c of criteria) {
		const stats = criterionStats(scores, c.id, optionId);
		if (stats.count > 0) {
			totalStdDev += stats.stdDev;
			counted++;
		}
	}
	if (counted === 0) return 0;
	const avgStdDev = totalStdDev / counted;
	return Math.max(0, 1 - avgStdDev / 4.5);
}

// -- Component --

declare global {
	interface HTMLElementTagNameMap {
		"folk-choice-spider": FolkChoiceSpider;
	}
}

const USER_COLORS = ["#7c5bf5", "#f59e0b", "#10b981", "#ef4444", "#06b6d4", "#ec4899", "#8b5cf6", "#f97316"];

function userColor(userId: string): string {
	let hash = 0;
	for (let i = 0; i < userId.length; i++) {
		hash = ((hash << 5) - hash + userId.charCodeAt(i)) | 0;
	}
	return USER_COLORS[Math.abs(hash) % USER_COLORS.length];
}

export class FolkChoiceSpider extends FolkShape {
	static override tagName = "folk-choice-spider";

	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 = "Score Options";
	#options: SpiderOption[] = [];
	#criteria: SpiderCriterion[] = [];
	#scores: SpiderScore[] = [];
	#userId = "";
	#userName = "";
	#selectedOptionId = "";
	#drawerOpen = false;
	#settingsOpen = false;
	#isSliding = false;

	// DOM refs
	#wrapperEl: HTMLElement | null = null;
	#bodyEl: HTMLElement | null = null;
	#chartArea: HTMLElement | null = null;
	#slidersEl: HTMLElement | null = null;
	#legendEl: HTMLElement | null = null;
	#summaryEl: HTMLElement | null = null;
	#optionTabsEl: 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: SpiderOption[]) {
		this.#options = v;
		if (v.length > 0 && !v.some((o) => o.id === this.#selectedOptionId)) {
			this.#selectedOptionId = v[0].id;
		}
		this.#render();
		this.requestUpdate("options");
	}

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

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

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

	#setScore(criterionId: string, value: number) {
		if (!this.#ensureIdentity()) return;
		const optionId = this.#selectedOptionId;

		// Remove existing score for this user/option/criterion
		this.#scores = this.#scores.filter(
			(s) => !(s.userId === this.#userId && s.optionId === optionId && s.criterionId === criterionId),
		);
		this.#scores.push({
			userId: this.#userId,
			userName: this.#userName,
			optionId,
			criterionId,
			value,
			timestamp: Date.now(),
		});

		this.#render();
		this.dispatchEvent(new CustomEvent("content-change"));
	}

	override createRenderRoot() {
		const root = super.createRenderRoot();
		this.initPorts();
		this.#ensureIdentity();
		if (this.#options.length > 0) this.#selectedOptionId = this.#options[0].id;

		const wrapper = document.createElement("div");
		wrapper.className = "wrapper";
		wrapper.innerHTML = html`
			<div class="header">
				<span class="header-title">
					<span>&#x1F578;</span>
					<span class="title-text">Spider</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="option-tabs"></div>
				<div class="chart-area"></div>
				<div class="sliders"></div>
				<div class="legend"></div>
				<div class="score-summary"></div>
				<div class="add-forms">
					<div class="add-group">
						<input type="text" class="add-opt-input" placeholder="+ option" />
						<button class="add-opt-btn">Add</button>
					</div>
					<div class="add-group">
						<input type="text" class="add-crit-input" placeholder="+ criterion" />
						<button class="add-crit-btn">Add</button>
					</div>
				</div>
			</div>
			<div class="results-drawer"></div>
			<div class="settings-panel"></div>
			<div class="username-prompt" style="display: none;">
				<p>Enter your name to score:</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.#chartArea = wrapper.querySelector(".chart-area") as HTMLElement;
		this.#slidersEl = wrapper.querySelector(".sliders") as HTMLElement;
		this.#legendEl = wrapper.querySelector(".legend") as HTMLElement;
		this.#summaryEl = wrapper.querySelector(".score-summary") as HTMLElement;
		this.#optionTabsEl = wrapper.querySelector(".option-tabs") 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 addOptInput = wrapper.querySelector(".add-opt-input") as HTMLInputElement;
		const addOptBtn = wrapper.querySelector(".add-opt-btn") as HTMLButtonElement;
		const addCritInput = wrapper.querySelector(".add-crit-input") as HTMLInputElement;
		const addCritBtn = wrapper.querySelector(".add-crit-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.#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(); });

		// Add option
		const addOpt = () => {
			const label = addOptInput.value.trim();
			if (!label) return;
			const id = `opt-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
			this.#options.push({ id, label });
			if (!this.#selectedOptionId) this.#selectedOptionId = id;
			addOptInput.value = "";
			this.#render();
			this.dispatchEvent(new CustomEvent("content-change"));
		};
		addOptBtn.addEventListener("click", (e) => { e.stopPropagation(); addOpt(); });
		addOptInput.addEventListener("click", (e) => e.stopPropagation());
		addOptInput.addEventListener("keydown", (e) => { e.stopPropagation(); if (e.key === "Enter") addOpt(); });

		// Add criterion
		const addCrit = () => {
			const label = addCritInput.value.trim();
			if (!label) return;
			const id = `crit-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
			this.#criteria.push({ id, label, weight: 1 });
			addCritInput.value = "";
			this.#render();
			this.dispatchEvent(new CustomEvent("content-change"));
		};
		addCritBtn.addEventListener("click", (e) => { e.stopPropagation(); addCrit(); });
		addCritInput.addEventListener("click", (e) => e.stopPropagation());
		addCritInput.addEventListener("keydown", (e) => { e.stopPropagation(); if (e.key === "Enter") addCrit(); });

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

		this.#render();
		return root;
	}

	#render() {
		this.#renderOptionTabs();
		this.#renderChart();
		if (!this.#isSliding) this.#renderSliders();
		this.#renderLegend();
		this.#renderSummary();
		if (this.#drawerOpen) this.#renderDrawer();

		// Emit decision data on output ports
		if (this.#options.length > 0 && this.#criteria.length > 0) {
			const results = this.#options.map(opt => ({
				id: opt.id,
				label: opt.label,
				score: weightedMeanScore(this.#scores, this.#criteria, opt.id),
			}));
			results.sort((a, b) => b.score - a.score);
			const winner = results[0];
			const allUsers = new Set(this.#scores.map(s => s.userId));

			this.setPortValue("results-out", results);
			this.setPortValue("decision-out", {
				decided: allUsers.size > 0 && winner.score > 0,
				winner: winner?.id ?? null,
				winnerLabel: winner?.label ?? null,
				passed: winner?.score > 0,
				margin: results.length >= 2 ? winner.score - results[1].score : 0,
				results,
				voters: allUsers.size,
			});
		}
	}

	#renderOptionTabs() {
		if (!this.#optionTabsEl) return;
		this.#optionTabsEl.innerHTML = this.#options
			.map((opt) => `<button class="option-tab ${opt.id === this.#selectedOptionId ? "active" : ""}" data-opt="${opt.id}">${this.#escapeHtml(opt.label)}</button>`)
			.join("");

		this.#optionTabsEl.querySelectorAll(".option-tab").forEach((tab) => {
			tab.addEventListener("click", (e) => {
				e.stopPropagation();
				this.#selectedOptionId = (tab as HTMLElement).dataset.opt!;
				this.#render();
			});
		});
	}

	#renderChart() {
		if (!this.#chartArea) return;
		const n = this.#criteria.length;
		if (n < 3) {
			this.#chartArea.innerHTML = '<div class="empty-state">Add at least 3 criteria</div>';
			return;
		}

		const CX = 130;
		const CY = 120;
		const R = 90;
		const RINGS = 5;
		const angleStep = (2 * Math.PI) / n;

		const polar = (angle: number, r: number) => {
			const a = angle - Math.PI / 2;
			return { x: CX + r * Math.cos(a), y: CY + r * Math.sin(a) };
		};

		let svg = `<svg viewBox="0 0 260 250" xmlns="http://www.w3.org/2000/svg">`;

		// Grid rings
		for (let ring = 1; ring <= RINGS; ring++) {
			const r = (R / RINGS) * ring;
			const pts = Array.from({ length: n }, (_, i) => {
				const p = polar(i * angleStep, r);
				return `${p.x},${p.y}`;
			}).join(" ");
			svg += `<polygon points="${pts}" fill="none" stroke="#e2e8f0" stroke-width="${ring === RINGS ? 1.5 : 0.5}"/>`;
		}

		// Axis lines + labels
		for (let i = 0; i < n; i++) {
			const end = polar(i * angleStep, R);
			const lbl = polar(i * angleStep, R + 16);
			svg += `<line x1="${CX}" y1="${CY}" x2="${end.x}" y2="${end.y}" stroke="#e2e8f0" stroke-width="0.5"/>`;
			svg += `<text x="${lbl.x}" y="${lbl.y}" text-anchor="middle" dominant-baseline="central" fill="#94a3b8" font-size="9">${this.#escapeHtml(this.#criteria[i].label)}</text>`;
		}

		// Get unique users who scored the selected option
		const optId = this.#selectedOptionId;
		const userIds = [...new Set(this.#scores.filter((s) => s.optionId === optId).map((s) => s.userId))];

		// Per-user polygons
		for (const uid of userIds) {
			const verts = getRadarVertices(this.#scores, this.#criteria, optId, uid, CX, CY, R);
			if (verts.length >= 3) {
				const pts = verts.map((v) => `${v.x},${v.y}`).join(" ");
				const color = userColor(uid);
				svg += `<polygon points="${pts}" fill="${color}" fill-opacity="0.15" stroke="${color}" stroke-width="1.5" stroke-opacity="0.6"/>`;
				for (const v of verts) {
					svg += `<circle cx="${v.x}" cy="${v.y}" r="3" fill="${color}"/>`;
				}
			}
		}

		// Average polygon (dashed)
		if (userIds.length > 0) {
			const avgVerts = getAverageRadarVertices(this.#scores, this.#criteria, optId, CX, CY, R);
			if (avgVerts.length >= 3) {
				const pts = avgVerts.map((v) => `${v.x},${v.y}`).join(" ");
				svg += `<polygon points="${pts}" fill="none" stroke="#1e293b" stroke-width="2" stroke-dasharray="4 3" opacity="0.5"/>`;
			}
		}

		svg += `</svg>`;
		this.#chartArea.innerHTML = svg;
	}

	#renderSliders() {
		if (!this.#slidersEl) return;
		const optId = this.#selectedOptionId;

		if (this.#criteria.length === 0) {
			this.#slidersEl.innerHTML = "";
			return;
		}

		this.#slidersEl.innerHTML = this.#criteria
			.map((c) => {
				const myScore = this.#scores.find(
					(s) => s.userId === this.#userId && s.optionId === optId && s.criterionId === c.id,
				);
				const val = myScore ? myScore.value : 5;
				return `
					<div class="slider-row">
						<span class="slider-label">${this.#escapeHtml(c.label)}</span>
						<input type="range" class="slider-input" min="1" max="10" value="${val}" data-crit="${c.id}" />
						<span class="slider-val">${val}</span>
					</div>
				`;
			})
			.join("");

		this.#slidersEl.querySelectorAll(".slider-input").forEach((slider) => {
			const input = slider as HTMLInputElement;
			input.addEventListener("click", (e) => e.stopPropagation());
			input.addEventListener("pointerdown", (e) => { e.stopPropagation(); this.#isSliding = true; });
			input.addEventListener("pointerup", () => { this.#isSliding = false; });
			input.addEventListener("lostpointercapture", () => { this.#isSliding = false; });
			input.addEventListener("input", (e) => {
				e.stopPropagation();
				const critId = input.dataset.crit!;
				const val = parseInt(input.value);
				const valEl = input.parentElement!.querySelector(".slider-val") as HTMLElement;
				valEl.textContent = String(val);
				this.#setScore(critId, val);
			});
		});
	}

	#renderLegend() {
		if (!this.#legendEl) return;
		const optId = this.#selectedOptionId;
		const users = new Map<string, string>();
		for (const s of this.#scores) {
			if (s.optionId === optId) users.set(s.userId, s.userName);
		}

		this.#legendEl.innerHTML = [...users.entries()]
			.map(([uid, name]) => `<span class="legend-item"><span class="legend-dot" style="background:${userColor(uid)}"></span>${this.#escapeHtml(name)}</span>`)
			.join("");
	}

	#renderSummary() {
		if (!this.#summaryEl) return;

		if (this.#options.length === 0 || this.#criteria.length === 0) {
			this.#summaryEl.innerHTML = "";
			return;
		}

		const results = this.#options.map((opt) => ({
			label: opt.label,
			score: weightedMeanScore(this.#scores, this.#criteria, opt.id),
		}));

		results.sort((a, b) => b.score - a.score);
		const best = results[0];

		if (best && best.score > 0) {
			const summary = results.map((r) => `${this.#escapeHtml(r.label)}: ${r.score.toFixed(1)}`).join(" | ");
			this.#summaryEl.innerHTML = `<span class="best">${this.#escapeHtml(best.label)}</span> leads &mdash; ${summary}`;
		} else {
			this.#summaryEl.innerHTML = "Score options to see results";
		}
	}

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

		// Group results: weighted mean per option
		const results = this.#options.map((opt) => ({
			id: opt.id,
			label: opt.label,
			score: weightedMeanScore(this.#scores, this.#criteria, opt.id),
		}));
		results.sort((a, b) => b.score - a.score);
		const maxScore = Math.max(1, ...results.map((r) => r.score));

		let resultsHtml = '<div class="drawer-section"><div class="drawer-heading">Group Results</div>';
		for (const r of results) {
			const pct = (r.score / maxScore) * 100;
			resultsHtml += `<div class="drawer-bar-row">
				<span class="drawer-bar-label">${this.#escapeHtml(r.label)}</span>
				<div class="drawer-bar-bg"><div class="drawer-bar-fill" style="width:${pct}%;background:#059669"></div></div>
				<span class="drawer-bar-val">${r.score.toFixed(1)}</span>
			</div>`;
		}
		if (results.length >= 2 && results[0].score > 0) {
			const margin = results[0].score - results[1].score;
			resultsHtml += `<div class="stat-row"><span class="stat-label">Margin</span><span class="stat-value">${margin.toFixed(1)}</span></div>`;
		}
		resultsHtml += "</div>";

		// Statistics
		const allUsers = new Set(this.#scores.map((s) => s.userId));
		let statsHtml = '<div class="drawer-section"><div class="drawer-heading">Statistics</div>';
		statsHtml += `<div class="stat-row"><span class="stat-label">Participants</span><span class="stat-value">${allUsers.size}</span></div>`;

		for (const opt of this.#options) {
			const ci = consensusIndex(this.#scores, this.#criteria, opt.id);
			const optScorers = new Set(this.#scores.filter((s) => s.optionId === opt.id).map((s) => s.userId));
			statsHtml += `<div class="stat-row"><span class="stat-label">${this.#escapeHtml(opt.label)} consensus</span><span class="stat-value">${(ci * 100).toFixed(0)}%</span></div>`;
			statsHtml += `<div class="stat-row"><span class="stat-label">${this.#escapeHtml(opt.label)} scorers</span><span class="stat-value">${optScorers.size}</span></div>`;
		}

		for (const c of this.#criteria) {
			const allVals: number[] = [];
			for (const opt of this.#options) {
				const stats = criterionStats(this.#scores, c.id, opt.id);
				if (stats.count > 0) allVals.push(stats.mean);
			}
			if (allVals.length > 0) {
				const avg = allVals.reduce((a, b) => a + b, 0) / allVals.length;
				statsHtml += `<div class="stat-row"><span class="stat-label">${this.#escapeHtml(c.label)} avg</span><span class="stat-value">${avg.toFixed(1)}</span></div>`;
			}
		}
		statsHtml += "</div>";

		// Participants
		const userMap = new Map<string, { name: string; optionsScored: number; lastActive: number }>();
		for (const s of this.#scores) {
			const u = userMap.get(s.userId) || { name: s.userName, optionsScored: 0, lastActive: 0 };
			u.name = s.userName;
			u.lastActive = Math.max(u.lastActive, s.timestamp);
			userMap.set(s.userId, u);
		}
		for (const [uid, u] of userMap) {
			const opts = new Set(this.#scores.filter((s) => s.userId === uid).map((s) => s.optionId));
			u.optionsScored = opts.size;
		}

		let participantsHtml = '<div class="drawer-section"><div class="drawer-heading">Participants</div>';
		for (const [uid, u] of userMap) {
			const ago = this.#timeAgo(u.lastActive);
			participantsHtml += `<div class="participant-row">
				<span class="participant-dot" style="background:${userColor(uid)}"></span>
				<span>${this.#escapeHtml(u.name)}</span>
				<span style="margin-left:auto;color:#94a3b8">${u.optionsScored} opt, ${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}" data-kind="opt">`;
			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">Criteria &amp; Weights</div>';
		for (let i = 0; i < this.#criteria.length; i++) {
			const c = this.#criteria[i];
			h += `<div class="settings-item" data-idx="${i}" data-kind="crit">`;
			h += `<input type="text" class="crit-label" value="${esc(c.label)}" />`;
			h += `<input type="range" class="crit-weight" min="1" max="5" value="${c.weight}" />`;
			h += `<span class="weight-val">${c.weight}</span>`;
			h += `<button class="remove-btn" title="Remove">&times;</button></div>`;
		}
		h += '</div>';
		const allUsers = new Set(this.#scores.map((s) => s.userId));
		h += '<div><div class="settings-label">Danger Zone</div>';
		h += `<button class="settings-danger clear-data-btn">Reset all scores (${allUsers.size} participant${allUsers.size !== 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(".crit-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.#criteria[idx].label = input.value;
				this.dispatchEvent(new CustomEvent("content-change"));
			});
		});
		this.#settingsEl.querySelectorAll(".crit-weight").forEach((el) => {
			const input = el as HTMLInputElement;
			input.addEventListener("click", stop);
			input.addEventListener("pointerdown", stop);
			input.addEventListener("input", () => {
				const idx = parseInt(input.closest(".settings-item")!.getAttribute("data-idx")!);
				this.#criteria[idx].weight = parseInt(input.value);
				const valEl = input.parentElement!.querySelector(".weight-val") as HTMLElement;
				valEl.textContent = input.value;
				this.dispatchEvent(new CustomEvent("content-change"));
			});
		});
		this.#settingsEl.querySelectorAll(".remove-btn").forEach((btn) => {
			btn.addEventListener("click", (e) => {
				e.stopPropagation();
				const item = (btn as HTMLElement).closest(".settings-item")!;
				const idx = parseInt(item.getAttribute("data-idx")!);
				const kind = item.getAttribute("data-kind");
				if (kind === "opt") {
					const removedId = this.#options[idx].id;
					this.#options.splice(idx, 1);
					this.#scores = this.#scores.filter((s) => s.optionId !== removedId);
					if (this.#selectedOptionId === removedId) {
						this.#selectedOptionId = this.#options[0]?.id || "";
					}
				} else {
					const removedId = this.#criteria[idx].id;
					this.#criteria.splice(idx, 1);
					this.#scores = this.#scores.filter((s) => s.criterionId !== removedId);
				}
				this.#renderSettings();
				this.dispatchEvent(new CustomEvent("content-change"));
			});
		});
		this.#settingsEl.querySelector(".clear-data-btn")!.addEventListener("click", (e) => {
			e.stopPropagation();
			this.#scores = [];
			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>): FolkChoiceSpider {
		const el = FolkShape.fromData(data) as FolkChoiceSpider;
		if (data.title !== undefined) el.title = data.title;
		if (data.options !== undefined) el.options = data.options;
		if (data.criteria !== undefined) el.criteria = data.criteria;
		if (data.scores !== undefined) el.scores = data.scores;
		return el;
	}

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

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