/**
 * <folk-crowdsurf-dashboard> — Tinder-style swipe UI for community activity proposals.
 *
 * Three views: Discover (swipe cards), Create (new prompt form), Profile (stats).
 * Multiplayer: uses CrowdSurfLocalFirstClient for real-time sync via Automerge.
 */

import { TourEngine } from '../../../shared/tour-engine';
import type { TourStep } from '../../../shared/tour-engine';
import { startPresenceHeartbeat } from '../../../shared/collab-presence';
import { CrowdSurfLocalFirstClient } from '../local-first-client';
import type { CrowdSurfDoc, CrowdSurfPrompt, Contribution } from '../schemas';
import { getDecayProgress, getTimeRemaining, getRightSwipeCount, isReadyToTrigger, getUrgency, parseContributions, crowdsurfSchema, crowdsurfDocId } from '../schemas';
import { getModuleApiBase } from "../../../shared/url-helpers";
import type { DocumentId } from "../../../shared/local-first/document";
import { ViewHistory } from "../../../shared/view-history.js";

// ── Auth helpers ──
function getSession(): { accessToken: string; claims: { sub: string; did?: string; username?: string } } | null {
	try {
		const raw = localStorage.getItem('encryptid_session');
		if (!raw) return null;
		const s = JSON.parse(raw);
		return s?.accessToken ? s : null;
	} catch { return null; }
}
function getMyDid(): string | null {
	const s = getSession();
	if (!s) return null;
	return (s.claims as any).did || s.claims.sub;
}

type ViewTab = 'discover' | 'create' | 'rank' | 'profile';

const CROWDSURF_TOUR_STEPS: TourStep[] = [
	{ target: '.cs-nav-btn[data-tab="discover"]', title: 'Discover', message: 'Swipe through nearby activities — join ones that interest you.' },
	{ target: '#cs-current-card', title: 'Swipe Cards', message: 'Swipe right to join or left to skip. You can also tap the buttons below.' },
	{ target: '.cs-nav-btn[data-tab="create"]', title: 'Create', message: 'Propose a new activity for your community.' },
	{ target: '.cs-nav-btn[data-tab="rank"]', title: 'Rank', message: 'Elo pairwise ranking to surface the best activities.' },
];

class FolkCrowdSurfDashboard extends HTMLElement {
	private shadow: ShadowRoot;
	private space: string;
	private _tour!: TourEngine;

	// State
	private activeTab: ViewTab = 'discover';
	private _history = new ViewHistory<ViewTab>('discover', 'crowdsurf');
	private loading = true;
	private prompts: CrowdSurfPrompt[] = [];
	private currentPromptIndex = 0;

	// Swipe state
	private isDragging = false;
	private startX = 0;
	private currentX = 0;
	private isAnimating = false;

	// Create form state
	private contributionSuggestions: string[] = [];

	// Profile stats
	private stats = { joined: 0, created: 0, triggered: 0 };

	// Rank (pairwise Elo) state
	private rankPairA: CrowdSurfPrompt | null = null;
	private rankPairB: CrowdSurfPrompt | null = null;
	private rankLoading = false;
	private rankSessionCount = 0;
	private rankLastResult: { winnerId: string; delta: number } | null = null;

	// Multiplayer
	private lfClient: CrowdSurfLocalFirstClient | null = null;
	private _lfcUnsub: (() => void) | null = null;
	private _subscribedDocIds: string[] = [];
	private _stopPresence: (() => void) | null = null;

	// Expiry timer
	private _expiryTimer: number | null = null;

	constructor() {
		super();
		this.shadow = this.attachShadow({ mode: 'open' });
		this.space = this.getAttribute('space') || 'demo';
		this._tour = new TourEngine(
			this.shadow,
			CROWDSURF_TOUR_STEPS,
			'crowdsurf_tour_done',
			() => this.shadow.querySelector('.cs-app') as HTMLElement,
		);
	}

	connectedCallback() {
		if (this.space === 'demo') {
			this.loadDemoData();
		} else {
			this.initMultiplayer();
		}
		this._stopPresence = startPresenceHeartbeat(() => ({ module: 'crowdsurf', context: this.prompts[this.currentPromptIndex]?.text || 'CrowdSurf' }));
		window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener);
	}

	disconnectedCallback() {
		this._history.destroy();
		window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener);
		this._stopPresence?.();
		this._lfcUnsub?.();
		this._lfcUnsub = null;
		this.lfClient?.disconnect();
		if (this._expiryTimer !== null) clearInterval(this._expiryTimer);
		const runtime = (window as any).__rspaceOfflineRuntime;
		if (runtime) {
			for (const id of this._subscribedDocIds) runtime.unsubscribe(id);
		}
		this._subscribedDocIds = [];
	}

	private _onViewRestored = (e: CustomEvent) => {
		if (e.detail?.moduleId !== 'crowdsurf') return;
		this.activeTab = e.detail.view;
		this.render();
		this.bindEvents();
	};

	// ── Multiplayer init ──

	private async initMultiplayer() {
		this.loading = true;
		this.render();

		try {
			this.lfClient = new CrowdSurfLocalFirstClient(this.space);
			await this.lfClient.init();
			await this.lfClient.subscribe();

			this._lfcUnsub = this.lfClient.onChange((doc) => {
				this.extractPrompts(doc);
				this.render();
				this.bindEvents();
			});

			const doc = this.lfClient.getDoc();
			if (doc) this.extractPrompts(doc);
		} catch (err) {
			console.warn('[CrowdSurf] Local-first init failed:', err);
		}

		this.loading = false;
		this.render();
		this.bindEvents();
		if (!localStorage.getItem('crowdsurf_tour_done')) {
			setTimeout(() => this._tour.start(), 800);
		}
		this.subscribeCollabOverlay();

		// Check expiry every 30s
		this._expiryTimer = window.setInterval(() => this.checkExpiry(), 30000);
	}

	private async subscribeCollabOverlay() {
		const runtime = (window as any).__rspaceOfflineRuntime;
		if (!runtime?.isInitialized) return;
		try {
			const docId = crowdsurfDocId(this.space) as DocumentId;
			await runtime.subscribe(docId, crowdsurfSchema);
			this._subscribedDocIds.push(docId);
		} catch { /* runtime unavailable */ }
	}

	private extractPrompts(doc: CrowdSurfDoc) {
		const myDid = getMyDid();
		const all = doc.prompts ? Object.values(doc.prompts) : [];

		// Sort: active first (by creation, newest first), then triggered, then expired
		this.prompts = all.sort((a, b) => {
			if (a.expired !== b.expired) return a.expired ? 1 : -1;
			if (a.triggered !== b.triggered) return a.triggered ? 1 : -1;
			return b.createdAt - a.createdAt;
		});

		// Compute profile stats
		if (myDid) {
			this.stats.created = all.filter(p => p.createdBy === myDid).length;
			this.stats.joined = all.filter(p => p.swipes[myDid]?.direction === 'right').length;
			this.stats.triggered = all.filter(p => p.triggered).length;
		}
	}

	private checkExpiry() {
		if (!this.lfClient) return;
		const doc = this.lfClient.getDoc();
		if (!doc?.prompts) return;
		for (const prompt of Object.values(doc.prompts)) {
			if (!prompt.expired && getDecayProgress(prompt) >= 1) {
				this.lfClient.markExpired(prompt.id);
			}
		}
	}

	// ── Demo data ──

	private loadDemoData() {
		const now = Date.now();
		this.prompts = [
			{
				id: 'demo-1', text: 'Community Garden Planting Day', location: 'Tempelhof Field, Berlin',
				threshold: 5, duration: 4, activityDuration: '3 hours', createdAt: now - 3600000,
				createdBy: null, triggered: false, expired: false,
				swipes: { 'alice': { direction: 'right', timestamp: now - 2000000, contribution: { bringing: ['seedlings', 'gardening gloves'], needed: ['watering cans'], tags: ['food'], value: 10 } }, 'bob': { direction: 'right', timestamp: now - 1800000 }, 'carol': { direction: 'right', timestamp: now - 1500000 } },
				elo: 1532, comparisons: 6, wins: 4,
			},
			{
				id: 'demo-2', text: 'Open Mic & Jam Session', location: 'Klunkerkranich rooftop',
				threshold: 8, duration: 6, activityDuration: '2 hours', createdAt: now - 7200000,
				createdBy: null, triggered: false, expired: false,
				swipes: { 'dave': { direction: 'right', timestamp: now - 5000000, contribution: { bringing: ['guitar', 'amp'], needed: ['microphone'], tags: ['music'], value: 15 } }, 'eve': { direction: 'right', timestamp: now - 4000000 }, 'frank': { direction: 'right', timestamp: now - 3500000 }, 'grace': { direction: 'right', timestamp: now - 3000000 }, 'hank': { direction: 'right', timestamp: now - 2500000 }, 'iris': { direction: 'right', timestamp: now - 2000000 } },
				elo: 1568, comparisons: 8, wins: 6,
			},
			{
				id: 'demo-3', text: 'Repair Cafe — Fix Your Stuff!', location: 'Maker Space, Kreuzberg',
				threshold: 3, duration: 2, activityDuration: '4 hours', createdAt: now - 600000,
				createdBy: null, triggered: false, expired: false,
				swipes: { 'jack': { direction: 'right', timestamp: now - 400000, contribution: { bringing: ['soldering iron', 'electronics skills'], needed: ['broken gadgets'], tags: ['tech'], value: 15 } } },
				elo: 1485, comparisons: 4, wins: 1,
			},
			{
				id: 'demo-4', text: 'Sunrise Yoga by the Canal', location: 'Landwehr Canal',
				threshold: 4, duration: 8, activityDuration: '1 hour', createdAt: now - 1000000,
				createdBy: null, triggered: false, expired: false,
				swipes: {},
				elo: 1415, comparisons: 6, wins: 1,
			},
		];
		this.loading = false;
		this.render();
		this.bindEvents();
		if (!localStorage.getItem('crowdsurf_tour_done')) {
			setTimeout(() => this._tour.start(), 800);
		}
	}

	startTour() { this._tour.start(); }

	// ── Swipe mechanics ──

	private getActivePrompts(): CrowdSurfPrompt[] {
		const myDid = getMyDid();
		return this.prompts.filter(p =>
			!p.expired && !p.triggered && (this.space === 'demo' || !myDid || !p.swipes[myDid])
		);
	}

	private getCurrentPrompt(): CrowdSurfPrompt | null {
		const active = this.getActivePrompts();
		if (this.currentPromptIndex >= active.length) this.currentPromptIndex = 0;
		return active[this.currentPromptIndex] ?? null;
	}

	private handleSwipeEnd(diffX: number) {
		if (Math.abs(diffX) < 100) return; // Below threshold
		const prompt = this.getCurrentPrompt();
		if (!prompt) return;

		if (diffX > 0) {
			this.performSwipe(prompt, 'right');
		} else {
			this.performSwipe(prompt, 'left');
		}
	}

	private performSwipe(prompt: CrowdSurfPrompt, direction: 'right' | 'left') {
		const myDid = getMyDid();

		if (this.space === 'demo') {
			// Demo mode: just advance
			if (direction === 'right') {
				prompt.swipes['demo-user'] = { direction: 'right', timestamp: Date.now() };
				this.stats.joined++;
				if (getRightSwipeCount(prompt) >= prompt.threshold) {
					prompt.triggered = true;
					this.stats.triggered++;
				}
			}
			this.currentPromptIndex++;
			this.isAnimating = true;
			setTimeout(() => {
				this.isAnimating = false;
				this.render();
				this.bindEvents();
			}, 300);
			return;
		}

		if (!myDid || !this.lfClient) return;

		// TODO: show contribution modal on right-swipe before committing
		this.lfClient.swipe(prompt.id, myDid, direction);
		this.currentPromptIndex++;
		this.isAnimating = true;
		setTimeout(() => {
			this.isAnimating = false;
			this.render();
			this.bindEvents();
		}, 300);
	}

	// ── Create prompt ──

	private handleCreate() {
		const getText = (id: string) => (this.shadow.getElementById(id) as HTMLInputElement | HTMLTextAreaElement)?.value?.trim() ?? '';
		const getNum = (id: string) => parseInt((this.shadow.getElementById(id) as HTMLInputElement)?.value ?? '0', 10);

		const text = getText('cs-text');
		const location = getText('cs-location');
		const threshold = getNum('cs-threshold') || 3;
		const duration = getNum('cs-duration') || 4;
		const activityDuration = getText('cs-activity-duration') || '1 hour';
		const bringing = getText('cs-bringing');
		const needed = getText('cs-needed');

		if (!text || !location) return;

		const prompt: CrowdSurfPrompt = {
			id: crypto.randomUUID(),
			text,
			location,
			threshold,
			duration,
			activityDuration,
			createdAt: Date.now(),
			createdBy: getMyDid(),
			triggered: false,
			expired: false,
			swipes: {},
			elo: 1500,
			comparisons: 0,
			wins: 0,
		};

		// Add creator's contribution as a right-swipe
		const myDid = getMyDid();
		if (myDid && (bringing || needed)) {
			const contribution = parseContributions(bringing, needed);
			prompt.swipes[myDid] = {
				direction: 'right',
				timestamp: Date.now(),
				contribution,
			};
		}

		if (this.space === 'demo') {
			this.prompts.unshift(prompt);
			this.stats.created++;
		} else if (this.lfClient) {
			this.lfClient.createPrompt(prompt);
		}

		this._history.push('discover');
		this.activeTab = 'discover';
		this.render();
		this.bindEvents();
	}

	// ── Render ──

	private render() {
		const isLive = this.lfClient?.isConnected ?? false;

		this.shadow.innerHTML = `
		<style>${this.getStyles()}</style>

		<div class="cs-app">
			${this.loading ? '<div class="cs-loading">Loading...</div>' : this.renderActiveView(isLive)}

			<!-- Bottom nav -->
			<nav class="cs-nav">
				<button class="cs-nav-btn${this.activeTab === 'discover' ? ' active' : ''}" data-tab="discover">
					<span class="cs-nav-icon">🏄</span>
					<span class="cs-nav-label">Discover</span>
				</button>
				<button class="cs-nav-btn${this.activeTab === 'create' ? ' active' : ''}" data-tab="create">
					<span class="cs-nav-icon">✨</span>
					<span class="cs-nav-label">Create</span>
				</button>
				<button class="cs-nav-btn${this.activeTab === 'rank' ? ' active' : ''}" data-tab="rank">
					<span class="cs-nav-icon">🎲</span>
					<span class="cs-nav-label">Rank</span>
				</button>
				<button class="cs-nav-btn${this.activeTab === 'profile' ? ' active' : ''}" data-tab="profile">
					<span class="cs-nav-icon">👤</span>
					<span class="cs-nav-label">Profile</span>
				</button>
			</nav>
		</div>`;
		this._tour.renderOverlay();
	}

	private renderActiveView(isLive: boolean): string {
		switch (this.activeTab) {
			case 'discover': return this.renderDiscover(isLive);
			case 'create': return this.renderCreateForm();
			case 'rank': return this.renderRank();
			case 'profile': return this.renderProfile();
		}
	}

	// ── Discover view (swipe cards) ──

	private renderDiscover(isLive: boolean): string {
		const prompt = this.getCurrentPrompt();
		const activeCount = this.getActivePrompts().length;
		const triggeredPrompts = this.prompts.filter(p => p.triggered);

		return `
		<div class="cs-header">
			<span class="cs-title">CrowdSurf</span>
			${isLive ? '<span class="cs-live"><span class="cs-live-dot"></span>LIVE</span>' : ''}
			${this.space === 'demo' ? '<span class="cs-demo-badge">DEMO</span>' : ''}
			<button style="margin-left:auto;padding:4px 10px;border-radius:6px;border:1px solid var(--rs-border);background:var(--rs-bg-surface);color:var(--rs-text-secondary);cursor:pointer;font-size:0.78rem;font-family:inherit;" id="btn-tour">Tour</button>
		</div>

		<div class="cs-discover">
			${prompt ? this.renderCard(prompt, activeCount) : this.renderNoCards()}

			${triggeredPrompts.length > 0 ? `
				<div class="cs-section-label">Triggered Activities</div>
				<div class="cs-triggered-list">
					${triggeredPrompts.map(p => `
						<div class="cs-triggered-card">
							<span class="cs-triggered-icon">🚀</span>
							<div class="cs-triggered-info">
								<div class="cs-triggered-title">${this.esc(p.text)}</div>
								<div class="cs-triggered-meta">📍 ${this.esc(p.location)} · ${getRightSwipeCount(p)} people</div>
							</div>
						</div>
					`).join('')}
				</div>
			` : ''}
		</div>`;
	}

	private renderCard(prompt: CrowdSurfPrompt, totalActive: number): string {
		const rightSwipes = getRightSwipeCount(prompt);
		const progressPct = Math.min((rightSwipes / prompt.threshold) * 100, 100);
		const urgency = getUrgency(prompt);
		const timeLeft = getTimeRemaining(prompt);

		// Collect contributions
		const allContribs = Object.values(prompt.swipes)
			.filter(s => s.direction === 'right' && s.contribution)
			.map(s => s.contribution!);

		const bringingAll = allContribs.flatMap(c => c.bringing);
		const neededAll = allContribs.flatMap(c => c.needed);

		return `
		<div class="cs-card-stack">
			<div class="cs-card" id="cs-current-card" data-collab-id="prompt:${prompt.id}">
				<div class="cs-swipe-indicator cs-swipe-left">✗ Pass</div>
				<div class="cs-swipe-indicator cs-swipe-right">✓ Join</div>

				<div class="cs-card-body">
					<div class="cs-card-text">${this.esc(prompt.text)}</div>
					${(prompt.elo ?? 0) > 0 ? `<span class="cs-elo-badge">⚡ ${prompt.elo ?? 1500}</span>` : ''}

					<div class="cs-card-location">📍 ${this.esc(prompt.location)}</div>
					${prompt.activityDuration ? `<div class="cs-card-duration">⏱️ ${this.esc(prompt.activityDuration)}</div>` : ''}

					${bringingAll.length > 0 ? `
						<div class="cs-contrib-section">
							<div class="cs-contrib-label">🎒 People are bringing:</div>
							<div class="cs-contrib-tags">${bringingAll.map(b => `<span class="cs-tag">${this.esc(b)}</span>`).join('')}</div>
						</div>
					` : ''}
					${neededAll.length > 0 ? `
						<div class="cs-contrib-section">
							<div class="cs-contrib-label">✨ Still needed:</div>
							<div class="cs-contrib-tags">${neededAll.map(n => `<span class="cs-tag needed">${this.esc(n)}</span>`).join('')}</div>
						</div>
					` : ''}

					<div class="cs-card-footer">
						<div class="cs-pool">${rightSwipes}/${prompt.threshold} interested</div>
						<div class="cs-progress-bar">
							<div class="cs-progress-fill" style="width:${progressPct}%"></div>
							${urgency === 'high' ? '<div class="cs-urgency-pulse"></div>' : ''}
						</div>
						<div class="cs-time ${urgency === 'high' ? 'urgent' : ''}">${timeLeft}</div>
					</div>

					${isReadyToTrigger(prompt) ? '<div class="cs-trigger-msg">🚀 Group ready! Activity happening!</div>' : ''}
				</div>
			</div>
			<div class="cs-card-count">${this.currentPromptIndex + 1} / ${totalActive}</div>
		</div>

		<div class="cs-swipe-buttons">
			<button class="cs-btn-skip" data-action="swipe-left">✗</button>
			<button class="cs-btn-join" data-action="swipe-right">✓</button>
		</div>`;
	}

	private renderNoCards(): string {
		return `
		<div class="cs-empty">
			<div class="cs-empty-icon">🏄</div>
			<p>No activities to discover right now.</p>
			<p>Create one and get the wave started!</p>
			<button class="cs-btn-create" data-action="go-create">Create Activity</button>
		</div>`;
	}

	// ── Create form ──

	private renderCreateForm(): string {
		return `
		<div class="cs-header">
			<span class="cs-title">New Activity</span>
		</div>
		<div class="cs-form">
			<label class="cs-label">What's happening?</label>
			<textarea id="cs-text" class="cs-input cs-textarea" rows="3" placeholder="Community garden planting, open mic night, repair cafe..."></textarea>

			<label class="cs-label">Where?</label>
			<input id="cs-location" type="text" class="cs-input" placeholder="Park, rooftop, community center...">

			<div class="cs-form-row">
				<div class="cs-form-col">
					<label class="cs-label">People needed</label>
					<input id="cs-threshold" type="number" class="cs-input" min="2" max="50" value="3">
				</div>
				<div class="cs-form-col">
					<label class="cs-label">Expires in (hours)</label>
					<input id="cs-duration" type="number" class="cs-input" min="1" max="168" value="4">
				</div>
			</div>

			<label class="cs-label">Activity duration</label>
			<select id="cs-activity-duration" class="cs-input">
				<option value="30 minutes">30 minutes</option>
				<option value="1 hour" selected>1 hour</option>
				<option value="2 hours">2 hours</option>
				<option value="3 hours">3 hours</option>
				<option value="half day">Half day</option>
				<option value="all day">All day</option>
			</select>

			<div class="cs-divider"></div>

			<label class="cs-label">🎒 What are you bringing?</label>
			<textarea id="cs-bringing" class="cs-input cs-textarea" rows="2" placeholder="Guitar, cooking skills, projector..."></textarea>

			<label class="cs-label">✨ What would be great to have?</label>
			<textarea id="cs-needed" class="cs-input cs-textarea" rows="2" placeholder="More musicians, ingredients, curious people..."></textarea>

			<button class="cs-btn-submit" data-action="create-prompt">Launch Activity 🚀</button>
		</div>`;
	}

	// ── Profile view ──

	private renderProfile(): string {
		const allTriggered = this.prompts.filter(p => p.triggered);
		const totalParticipants = this.prompts.reduce((sum, p) => sum + getRightSwipeCount(p), 0);

		return `
		<div class="cs-header">
			<span class="cs-title">Profile</span>
		</div>
		<div class="cs-profile">
			<div class="cs-stats-grid">
				<div class="cs-stat-card">
					<div class="cs-stat-num">${this.stats.joined}</div>
					<div class="cs-stat-label">Joined</div>
				</div>
				<div class="cs-stat-card">
					<div class="cs-stat-num">${this.stats.created}</div>
					<div class="cs-stat-label">Created</div>
				</div>
				<div class="cs-stat-card">
					<div class="cs-stat-num">${this.stats.triggered}</div>
					<div class="cs-stat-label">Triggered</div>
				</div>
			</div>

			<div class="cs-section-label">Community Stats</div>
			<div class="cs-community-stats">
				<div class="cs-community-row"><span>Active prompts</span><span>${this.prompts.filter(p => !p.expired && !p.triggered).length}</span></div>
				<div class="cs-community-row"><span>Total triggered</span><span>${allTriggered.length}</span></div>
				<div class="cs-community-row"><span>Total participants</span><span>${totalParticipants}</span></div>
			</div>
		</div>`;
	}

	// ── Rank (pairwise Elo) view ──

	private renderRank(): string {
		const activePrompts = this.prompts.filter(p => !p.expired && !p.triggered);
		const leaderboard = [...activePrompts].sort((a, b) => (b.elo ?? 1500) - (a.elo ?? 1500));

		const pairHtml = this.rankLoading
			? '<div class="cs-rank-loading">Loading pair...</div>'
			: (this.rankPairA && this.rankPairB)
				? `<div class="cs-rank-pair">
					<div class="cs-rank-card" data-rank-pick="${this.rankPairA.id}">
						<div class="cs-rank-card-text">${this.esc(this.rankPairA.text)}</div>
						<div class="cs-rank-card-loc">📍 ${this.esc(this.rankPairA.location)}</div>
						<div class="cs-rank-card-elo">⚡ ${this.rankPairA.elo ?? 1500}</div>
					</div>
					<div class="cs-rank-vs">VS</div>
					<div class="cs-rank-card" data-rank-pick="${this.rankPairB.id}">
						<div class="cs-rank-card-text">${this.esc(this.rankPairB.text)}</div>
						<div class="cs-rank-card-loc">📍 ${this.esc(this.rankPairB.location)}</div>
						<div class="cs-rank-card-elo">⚡ ${this.rankPairB.elo ?? 1500}</div>
					</div>
				</div>
				<div class="cs-rank-actions">
					<button class="cs-rank-skip" data-action="rank-skip">Skip</button>
					<span class="cs-rank-count">${this.rankSessionCount} comparisons</span>
				</div>`
				: (activePrompts.length < 2
					? '<div class="cs-rank-empty">Need at least 2 active prompts to rank.</div>'
					: '<div class="cs-rank-empty"><button class="cs-btn-create" data-action="rank-start">Start Ranking</button></div>');

		return `
		<div class="cs-header">
			<span class="cs-title">Rank Activities</span>
		</div>
		<div class="cs-rank-view">
			<p class="cs-rank-hint">Tap the activity you think should be higher priority.</p>
			${pairHtml}

			${leaderboard.length > 0 ? `
				<div class="cs-section-label">Elo Leaderboard</div>
				<div class="cs-leaderboard">
					${leaderboard.map((p, i) => `
						<div class="cs-lb-row${this.rankLastResult?.winnerId === p.id ? ' cs-lb-winner' : ''}" data-collab-id="response:${p.id}">
							<span class="cs-lb-rank">#${i + 1}</span>
							<span class="cs-lb-text">${this.esc(p.text)}</span>
							<span class="cs-lb-elo">⚡ ${p.elo ?? 1500}</span>
							<span class="cs-lb-comp">${p.comparisons ?? 0} cmp</span>
						</div>
					`).join('')}
				</div>
			` : ''}
		</div>`;
	}

	private loadRankPair() {
		const active = this.prompts.filter(p => !p.expired && !p.triggered);
		if (active.length < 2) { this.rankPairA = null; this.rankPairB = null; return; }

		if (this.space !== 'demo') {
			// Use API for live mode
			this.rankLoading = true;
			this.render();
			this.bindEvents();
			fetch(`${getModuleApiBase("crowdsurf")}/api/crowdsurf/pair?space=${this.space}`)
				.then(r => r.json())
				.then(data => {
					if (data.a && data.b) {
						this.rankPairA = data.a;
						this.rankPairB = data.b;
					}
				})
				.catch(() => {})
				.finally(() => {
					this.rankLoading = false;
					this.render();
					this.bindEvents();
				});
			return;
		}

		// Demo mode: sortition-weighted random pair
		const maxComp = Math.max(...active.map(p => p.comparisons ?? 0), 1);
		const weights = active.map(p => maxComp - (p.comparisons ?? 0) + 1);
		const totalWeight = weights.reduce((a, b) => a + b, 0);

		const pickWeighted = (excludeId?: string): CrowdSurfPrompt => {
			const effectiveWeight = excludeId ? totalWeight - (weights[active.findIndex(p => p.id === excludeId)] || 0) : totalWeight;
			let r = Math.random() * effectiveWeight;
			for (let i = 0; i < active.length; i++) {
				if (active[i].id === excludeId) continue;
				r -= weights[i];
				if (r <= 0) return active[i];
			}
			return active[active.length - 1];
		};

		this.rankPairA = pickWeighted();
		this.rankPairB = pickWeighted(this.rankPairA.id);
		this.render();
		this.bindEvents();
	}

	private submitRankComparison(winnerId: string) {
		const loserId = winnerId === this.rankPairA?.id ? this.rankPairB?.id : this.rankPairA?.id;
		if (!loserId) return;

		if (this.space === 'demo') {
			// Local Elo update
			const winner = this.prompts.find(p => p.id === winnerId);
			const loser = this.prompts.find(p => p.id === loserId);
			if (!winner || !loser) return;

			const wElo = winner.elo ?? 1500;
			const lElo = loser.elo ?? 1500;
			const expected = 1 / (1 + Math.pow(10, (lElo - wElo) / 400));
			const delta = Math.round(32 * (1 - expected));

			winner.elo = wElo + delta;
			winner.comparisons = (winner.comparisons ?? 0) + 1;
			winner.wins = (winner.wins ?? 0) + 1;
			loser.elo = lElo - delta;
			loser.comparisons = (loser.comparisons ?? 0) + 1;

			this.rankLastResult = { winnerId, delta };
			this.rankSessionCount++;
			// Flash result then load next pair
			this.render();
			this.bindEvents();
			setTimeout(() => {
				this.rankLastResult = null;
				this.loadRankPair();
			}, 600);
			return;
		}

		// Live mode: POST to API
		fetch(`${getModuleApiBase("crowdsurf")}/api/crowdsurf/compare?space=${this.space}`, {
			method: 'POST',
			headers: { 'Content-Type': 'application/json' },
			body: JSON.stringify({ winnerId, loserId }),
		})
			.then(r => r.json())
			.then(data => {
				if (data.ok) {
					this.rankLastResult = { winnerId, delta: data.winner.delta };
					this.rankSessionCount++;
					this.render();
					this.bindEvents();
					setTimeout(() => {
						this.rankLastResult = null;
						this.loadRankPair();
					}, 600);
				}
			})
			.catch(console.error);
	}

	// ── Event binding ──

	private bindEvents() {
		// Tab navigation
		this.shadow.querySelectorAll<HTMLElement>('.cs-nav-btn').forEach(btn => {
			btn.addEventListener('click', () => {
				const tab = btn.dataset.tab as ViewTab;
				if (tab && tab !== this.activeTab) {
					this._history.push(tab);
					this.activeTab = tab;
					this.render();
					this.bindEvents();
				}
			});
		});

		// Swipe gesture on card
		const card = this.shadow.getElementById('cs-current-card');
		if (card) this.setupSwipeGestures(card);

		// Swipe buttons
		this.shadow.querySelector('[data-action="swipe-left"]')?.addEventListener('click', () => {
			const prompt = this.getCurrentPrompt();
			if (prompt) this.performSwipe(prompt, 'left');
		});
		this.shadow.querySelector('[data-action="swipe-right"]')?.addEventListener('click', () => {
			const prompt = this.getCurrentPrompt();
			if (prompt) this.performSwipe(prompt, 'right');
		});

		// Go to create tab
		this.shadow.querySelector('[data-action="go-create"]')?.addEventListener('click', () => {
			this._history.push('create');
			this.activeTab = 'create';
			this.render();
			this.bindEvents();
		});

		// Create form submit
		this.shadow.querySelector('[data-action="create-prompt"]')?.addEventListener('click', () => this.handleCreate());

		// Rank: pick a winner
		this.shadow.querySelectorAll<HTMLElement>('[data-rank-pick]').forEach(card => {
			card.addEventListener('click', () => {
				const winnerId = card.dataset.rankPick;
				if (winnerId) this.submitRankComparison(winnerId);
			});
		});

		// Rank: skip
		this.shadow.querySelector('[data-action="rank-skip"]')?.addEventListener('click', () => this.loadRankPair());

		// Rank: start
		this.shadow.querySelector('[data-action="rank-start"]')?.addEventListener('click', () => this.loadRankPair());

		// Tour button
		this.shadow.getElementById('btn-tour')?.addEventListener('click', () => this.startTour());
	}

	private setupSwipeGestures(card: HTMLElement) {
		const handleStart = (clientX: number) => {
			if (this.isAnimating) return;
			this.startX = clientX;
			this.currentX = clientX;
			this.isDragging = true;
		};

		const handleMove = (clientX: number) => {
			if (!this.isDragging || this.isAnimating) return;
			this.currentX = clientX;
			const diffX = this.currentX - this.startX;
			const rotation = diffX * 0.1;
			card.style.transform = `translateX(${diffX}px) rotate(${rotation}deg)`;

			const leftInd = card.querySelector('.cs-swipe-left') as HTMLElement;
			const rightInd = card.querySelector('.cs-swipe-right') as HTMLElement;

			if (diffX < -50) {
				leftInd?.classList.add('show');
				rightInd?.classList.remove('show');
			} else if (diffX > 50) {
				rightInd?.classList.add('show');
				leftInd?.classList.remove('show');
			} else {
				leftInd?.classList.remove('show');
				rightInd?.classList.remove('show');
			}
		};

		const handleEnd = () => {
			if (!this.isDragging || this.isAnimating) return;
			this.isDragging = false;
			const diffX = this.currentX - this.startX;

			card.querySelector('.cs-swipe-left')?.classList.remove('show');
			card.querySelector('.cs-swipe-right')?.classList.remove('show');

			if (Math.abs(diffX) > 100) {
				// Animate out
				const direction = diffX > 0 ? 1 : -1;
				card.style.transition = 'transform 0.3s ease-out, opacity 0.3s ease-out';
				card.style.transform = `translateX(${direction * 500}px) rotate(${direction * 30}deg)`;
				card.style.opacity = '0';
				this.handleSwipeEnd(diffX);
			} else {
				card.style.transition = 'transform 0.2s ease-out';
				card.style.transform = '';
				setTimeout(() => { card.style.transition = ''; }, 200);
			}
		};

		// Pointer events (unified touch + mouse)
		card.addEventListener('pointerdown', (e: PointerEvent) => {
			if (e.button !== 0) return;
			card.setPointerCapture(e.pointerId);
			card.style.touchAction = 'none';
			handleStart(e.clientX);
		});
		card.addEventListener('pointermove', (e: PointerEvent) => {
			e.preventDefault();
			handleMove(e.clientX);
		});
		card.addEventListener('pointerup', () => handleEnd());
		card.addEventListener('pointercancel', () => {
			this.isDragging = false;
			card.style.transform = '';
			card.style.touchAction = '';
		});
	}

	// ── Styles ──

	private getStyles(): string {
		return `
		:host { display: block; height: 100%; -webkit-tap-highlight-color: transparent; }
		button, a, input, select, textarea, [role="button"] { touch-action: manipulation; }
		* { box-sizing: border-box; }

		.cs-app {
			display: flex; flex-direction: column; height: 100%;
			background: var(--rs-bg-page);
			color: var(--rs-text-primary);
		}

		.cs-loading { text-align: center; padding: 4rem; color: var(--rs-text-muted); flex: 1; display: flex; align-items: center; justify-content: center; }

		/* Header */
		.cs-header { display: flex; align-items: center; gap: 8px; padding: 1rem 1.25rem 0.5rem; }
		.cs-title { font-size: 1.1rem; font-weight: 700; flex: 1; }
		.cs-live { font-size: 0.7rem; padding: 2px 8px; border-radius: 999px; background: rgba(34,197,94,0.15); color: #22c55e; font-weight: 500; display: flex; align-items: center; gap: 3px; }
		.cs-live-dot { width: 6px; height: 6px; border-radius: 50%; background: #22c55e; animation: cs-pulse 2s infinite; }
		@keyframes cs-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
		.cs-demo-badge { font-size: 0.7rem; padding: 2px 8px; border-radius: 999px; background: var(--rs-primary); color: #fff; font-weight: 500; }

		/* Discover */
		.cs-discover { flex: 1; display: flex; flex-direction: column; padding: 0.5rem 1.25rem 1rem; overflow-y: auto; }

		/* Card stack */
		.cs-card-stack { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 320px; position: relative; }
		.cs-card {
			width: 100%; max-width: 380px;
			background: linear-gradient(135deg, rgba(102,126,234,0.15), rgba(118,75,162,0.15));
			border: 1px solid var(--rs-border);
			border-radius: 16px;
			padding: 1.5rem;
			position: relative;
			cursor: grab;
			user-select: none;
			transition: box-shadow 0.2s;
		}
		.cs-card:active { cursor: grabbing; }
		.cs-card:hover { box-shadow: 0 8px 32px rgba(0,0,0,0.2); }
		.cs-card-count { font-size: 0.75rem; color: var(--rs-text-muted); margin-top: 0.75rem; text-align: center; }

		/* Swipe indicators */
		.cs-swipe-indicator {
			position: absolute; top: 1.5rem;
			font-size: 1.25rem; font-weight: 800;
			padding: 6px 16px; border-radius: 8px;
			opacity: 0; transition: opacity 0.15s;
			pointer-events: none; z-index: 2;
		}
		.cs-swipe-left { left: 1rem; color: #ef4444; border: 2px solid #ef4444; }
		.cs-swipe-right { right: 1rem; color: #22c55e; border: 2px solid #22c55e; }
		.cs-swipe-indicator.show { opacity: 1; }

		/* Card body */
		.cs-card-body { position: relative; z-index: 1; }
		.cs-card-text { font-size: 1.2rem; font-weight: 700; margin-bottom: 1rem; line-height: 1.4; }
		.cs-card-location { font-size: 0.9rem; margin-bottom: 0.5rem; opacity: 0.85; }
		.cs-card-duration { font-size: 0.85rem; margin-bottom: 1rem; opacity: 0.7; background: rgba(255,255,255,0.08); display: inline-block; padding: 4px 10px; border-radius: 8px; }

		/* Contributions */
		.cs-contrib-section { margin-bottom: 0.75rem; }
		.cs-contrib-label { font-size: 0.78rem; color: var(--rs-text-secondary); margin-bottom: 4px; }
		.cs-contrib-tags { display: flex; flex-wrap: wrap; gap: 4px; }
		.cs-tag { font-size: 0.75rem; padding: 3px 8px; border-radius: 999px; background: rgba(94,234,212,0.12); color: #5eead4; }
		.cs-tag.needed { background: rgba(251,191,36,0.12); color: #fbbf24; }

		/* Card footer */
		.cs-card-footer { margin-top: 0.75rem; }
		.cs-pool { font-size: 0.85rem; font-weight: 600; margin-bottom: 6px; text-align: center; }
		.cs-progress-bar { height: 6px; border-radius: 999px; background: rgba(255,255,255,0.1); overflow: hidden; position: relative; }
		.cs-progress-fill { height: 100%; border-radius: 999px; background: linear-gradient(90deg, #667eea, #764ba2); transition: width 0.5s ease-out; }
		.cs-urgency-pulse { position: absolute; inset: 0; border-radius: 999px; background: rgba(239,68,68,0.3); animation: cs-urgency 1s infinite; }
		@keyframes cs-urgency { 0%,100% { opacity: 0; } 50% { opacity: 1; } }
		.cs-time { font-size: 0.78rem; text-align: center; margin-top: 6px; color: var(--rs-text-secondary); }
		.cs-time.urgent { color: #ef4444; animation: cs-blink 1s infinite; }
		@keyframes cs-blink { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
		.cs-trigger-msg { text-align: center; margin-top: 0.75rem; padding: 8px; border-radius: 8px; background: rgba(34,197,94,0.15); color: #22c55e; font-weight: 600; font-size: 0.9rem; }

		/* Swipe buttons */
		.cs-swipe-buttons { display: flex; justify-content: center; gap: 2rem; padding: 1rem 0; }
		.cs-btn-skip, .cs-btn-join {
			width: 56px; height: 56px; border-radius: 50%; border: 2px solid;
			font-size: 1.5rem; cursor: pointer; display: flex; align-items: center; justify-content: center;
			background: var(--rs-bg-surface); transition: all 0.15s; font-family: inherit;
		}
		.cs-btn-skip { border-color: #ef4444; color: #ef4444; }
		.cs-btn-skip:hover { background: rgba(239,68,68,0.15); }
		.cs-btn-join { border-color: #22c55e; color: #22c55e; }
		.cs-btn-join:hover { background: rgba(34,197,94,0.15); }

		/* Triggered list */
		.cs-section-label { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--rs-text-muted); margin: 1.25rem 0 0.5rem; font-weight: 600; }
		.cs-triggered-list { display: flex; flex-direction: column; gap: 6px; }
		.cs-triggered-card { display: flex; align-items: center; gap: 10px; padding: 0.75rem; background: var(--rs-bg-surface); border: 1px solid rgba(34,197,94,0.2); border-radius: 10px; }
		.cs-triggered-icon { font-size: 1.25rem; }
		.cs-triggered-info { flex: 1; min-width: 0; }
		.cs-triggered-title { font-weight: 600; font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
		.cs-triggered-meta { font-size: 0.78rem; color: var(--rs-text-secondary); }

		/* Empty state */
		.cs-empty { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; color: var(--rs-text-muted); padding: 2rem; }
		.cs-empty-icon { font-size: 3rem; margin-bottom: 1rem; }
		.cs-empty p { margin: 0.25rem 0; font-size: 0.9rem; }
		.cs-btn-create { margin-top: 1rem; padding: 0.6rem 1.5rem; border-radius: 10px; border: 1px solid var(--rs-primary); background: var(--rs-primary); color: #fff; font-size: 0.9rem; cursor: pointer; font-family: inherit; }

		/* Create form */
		.cs-form { flex: 1; padding: 0.5rem 1.25rem 2rem; overflow-y: auto; }
		.cs-label { display: block; font-size: 0.8rem; font-weight: 600; color: var(--rs-text-secondary); margin: 1rem 0 0.35rem; }
		.cs-label:first-child { margin-top: 0; }
		.cs-input { width: 100%; padding: 0.6rem 0.75rem; border-radius: 8px; border: 1px solid var(--rs-border); background: var(--rs-bg-surface); color: var(--rs-text-primary); font-size: 0.9rem; font-family: inherit; }
		.cs-input:focus { outline: none; border-color: var(--rs-primary); }
		.cs-textarea { resize: vertical; }
		.cs-form-row { display: flex; gap: 0.75rem; }
		.cs-form-col { flex: 1; }
		.cs-divider { border: none; border-top: 1px solid var(--rs-border); margin: 1.25rem 0; height: 0; }
		.cs-btn-submit { width: 100%; padding: 0.75rem; border-radius: 10px; border: none; background: linear-gradient(135deg, #667eea, #764ba2); color: #fff; font-size: 1rem; font-weight: 600; cursor: pointer; margin-top: 1.5rem; font-family: inherit; }
		.cs-btn-submit:hover { opacity: 0.9; }

		/* Profile */
		.cs-profile { flex: 1; padding: 0.5rem 1.25rem 2rem; overflow-y: auto; }
		.cs-stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.75rem; margin-bottom: 1.5rem; }
		.cs-stat-card { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 12px; padding: 1rem; text-align: center; }
		.cs-stat-num { font-size: 1.5rem; font-weight: 800; background: linear-gradient(135deg, #667eea, #764ba2); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
		.cs-stat-label { font-size: 0.75rem; color: var(--rs-text-secondary); margin-top: 2px; }
		.cs-community-stats { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 10px; overflow: hidden; }
		.cs-community-row { display: flex; justify-content: space-between; padding: 0.6rem 1rem; border-bottom: 1px solid var(--rs-border); font-size: 0.85rem; }
		.cs-community-row:last-child { border-bottom: none; }
		.cs-community-row span:last-child { font-weight: 600; }

		/* Elo badge on discover cards */
		.cs-elo-badge { display: inline-block; font-size: 0.75rem; padding: 2px 8px; border-radius: 999px; background: rgba(251,191,36,0.15); color: #fbbf24; font-weight: 600; margin-bottom: 0.5rem; }

		/* Rank view */
		.cs-rank-view { flex: 1; padding: 0.5rem 1.25rem 1rem; overflow-y: auto; }
		.cs-rank-hint { font-size: 0.82rem; color: var(--rs-text-secondary); margin: 0 0 1rem; text-align: center; }
		.cs-rank-loading { text-align: center; padding: 2rem; color: var(--rs-text-muted); }
		.cs-rank-empty { text-align: center; padding: 2rem; color: var(--rs-text-muted); }

		.cs-rank-pair { display: flex; gap: 12px; align-items: stretch; margin-bottom: 1rem; }
		.cs-rank-card {
			flex: 1; padding: 1.25rem; border-radius: 14px;
			background: var(--rs-bg-surface); border: 2px solid var(--rs-border);
			cursor: pointer; transition: all 0.15s; min-height: 140px;
			display: flex; flex-direction: column; justify-content: center;
		}
		.cs-rank-card:hover { border-color: #667eea; box-shadow: 0 4px 20px rgba(102,126,234,0.2); transform: translateY(-2px); }
		.cs-rank-card:active { transform: scale(0.97); }
		.cs-rank-card-text { font-size: 1rem; font-weight: 700; margin-bottom: 0.5rem; line-height: 1.3; }
		.cs-rank-card-loc { font-size: 0.8rem; opacity: 0.7; margin-bottom: 0.5rem; }
		.cs-rank-card-elo { font-size: 0.78rem; color: #fbbf24; font-weight: 600; }
		.cs-rank-vs { display: flex; align-items: center; font-size: 0.85rem; font-weight: 800; color: var(--rs-text-muted); padding: 0 4px; flex-shrink: 0; }

		.cs-rank-actions { display: flex; align-items: center; justify-content: center; gap: 1rem; margin-bottom: 1.25rem; }
		.cs-rank-skip { padding: 6px 16px; border-radius: 8px; border: 1px solid var(--rs-border); background: var(--rs-bg-surface); color: var(--rs-text-secondary); cursor: pointer; font-size: 0.82rem; font-family: inherit; }
		.cs-rank-skip:hover { border-color: var(--rs-text-muted); }
		.cs-rank-count { font-size: 0.78rem; color: var(--rs-text-muted); }

		/* Leaderboard */
		.cs-leaderboard { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 10px; overflow: hidden; }
		.cs-lb-row { display: flex; align-items: center; gap: 8px; padding: 0.6rem 0.75rem; border-bottom: 1px solid var(--rs-border); font-size: 0.85rem; transition: background 0.3s; }
		.cs-lb-row:last-child { border-bottom: none; }
		.cs-lb-row.cs-lb-winner { background: rgba(34,197,94,0.1); }
		.cs-lb-rank { font-weight: 700; color: var(--rs-text-muted); width: 28px; flex-shrink: 0; }
		.cs-lb-text { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 500; }
		.cs-lb-elo { font-weight: 600; color: #fbbf24; font-size: 0.8rem; flex-shrink: 0; }
		.cs-lb-comp { font-size: 0.72rem; color: var(--rs-text-muted); flex-shrink: 0; width: 48px; text-align: right; }

		@media (max-width: 480px) {
			.cs-rank-pair { flex-direction: column; }
			.cs-rank-vs { justify-content: center; padding: 4px 0; }
		}

		/* Bottom nav */
		.cs-nav { display: flex; border-top: 1px solid var(--rs-border); background: var(--rs-bg-surface); padding: 0.35rem 0 env(safe-area-inset-bottom, 0.35rem); flex-shrink: 0; }
		.cs-nav-btn { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 0.4rem 0; border: none; background: none; color: var(--rs-text-muted); cursor: pointer; font-family: inherit; transition: color 0.15s; }
		.cs-nav-btn:hover, .cs-nav-btn.active { color: var(--rs-text-primary); }
		.cs-nav-btn.active .cs-nav-icon { transform: scale(1.15); }
		.cs-nav-icon { font-size: 1.25rem; transition: transform 0.15s; }
		.cs-nav-label { font-size: 0.65rem; font-weight: 500; }

		@media (max-width: 480px) {
			.cs-card { padding: 1.25rem; }
			.cs-card-text { font-size: 1.05rem; }
			.cs-form { padding: 0.5rem 1rem 2rem; }
		}
		`;
	}

	private esc(s: string): string {
		const d = document.createElement('div');
		d.textContent = s;
		return d.innerHTML;
	}
}

customElements.define('folk-crowdsurf-dashboard', FolkCrowdSurfDashboard);
