/**
 * <folk-photo-gallery> — Community photo gallery powered by Immich.
 *
 * Role-aware: upload (member+), delete (moderator+), Immich embed (admin).
 * Shows setup prompt for admins when rPhotos isn't enabled for the space.
 *
 * Attributes:
 *   space — space slug (default: "demo")
 */

import { makeDraggableAll } from "../../../shared/draggable";
import { TourEngine } from "../../../shared/tour-engine";
import { ViewHistory } from "../../../shared/view-history.js";
import { startPresenceHeartbeat } from '../../../shared/collab-presence';
import type { DocumentId } from "../../../shared/local-first/document";
import { photosSchema, photosDocId } from "../schemas";

type UserRole = 'viewer' | 'member' | 'moderator' | 'admin' | null;

interface SpaceAlbumInfo {
	id: string;
	name: string;
	immichAlbumId: string;
	createdBy: string;
	createdAt: number;
}

interface Album {
	id: string;
	albumName: string;
	description: string;
	assetCount: number;
	albumThumbnailAssetId: string | null;
	updatedAt: string;
	shared: boolean;
}

interface Asset {
	id: string;
	type: string;
	originalFileName: string;
	exifInfo?: {
		city?: string;
		country?: string;
		dateTimeOriginal?: string;
		make?: string;
		model?: string;
	};
	fileCreatedAt: string;
}

const ROLE_LEVELS: Record<string, number> = { viewer: 0, member: 1, moderator: 2, admin: 3 };
function roleAtLeast(actual: string | null, required: string): boolean {
	if (!actual) return false;
	return (ROLE_LEVELS[actual] ?? 0) >= (ROLE_LEVELS[required] ?? 0);
}

class FolkPhotoGallery extends HTMLElement {
	private shadow: ShadowRoot;
	private space = "demo";
	private view: "gallery" | "album" | "lightbox" = "gallery";
	private albums: (Album | SpaceAlbumInfo)[] = [];
	private assets: Asset[] = [];
	private albumAssets: Asset[] = [];
	private selectedAlbum: (Album | SpaceAlbumInfo) | null = null;
	private lightboxAsset: Asset | null = null;
	private loading = false;
	private error = "";
	private showingSampleData = false;
	private userRole: UserRole = null;
	private spaceEnabled = false;
	private uploading = false;
	private _tour!: TourEngine;
	private _stopPresence: (() => void) | null = null;
	private _offlineUnsub: (() => void) | null = null;
	private _subscribedDocIds: string[] = [];
	private _history = new ViewHistory<"gallery" | "album" | "lightbox">("gallery", "rphotos");
	private static readonly TOUR_STEPS = [
		{ target: '.album-card', title: "Albums", message: "Browse shared photo albums — click one to see its photos.", advanceOnClick: false },
		{ target: '.photo-cell', title: "Photo Grid", message: "Click any photo to open it in the lightbox viewer.", advanceOnClick: false },
		{ target: '.rapp-nav__btn', title: "Actions", message: "Upload photos, open albums, or manage your gallery.", advanceOnClick: false },
	];

	constructor() {
		super();
		this.shadow = this.attachShadow({ mode: "open" });
		this._tour = new TourEngine(
			this.shadow,
			FolkPhotoGallery.TOUR_STEPS,
			"rphotos_tour_done",
			() => this.shadow.host as HTMLElement,
		);
	}

	connectedCallback() {
		this.space = this.getAttribute("space") || "demo";
		if (this.space === "demo") {
			this.loadDemoData();
		} else {
			this.loadSetupAndGallery();
			this.subscribeOffline();
		}
		if (!localStorage.getItem("rphotos_tour_done")) {
			setTimeout(() => this._tour.start(), 1200);
		}
		this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rphotos', context: this.selectedAlbum ? ('name' in this.selectedAlbum ? this.selectedAlbum.name : (this.selectedAlbum as Album).albumName) : 'Photos' }));
		window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener);
	}

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

	private _onViewRestored = (e: CustomEvent) => {
		if (e.detail?.moduleId !== 'rphotos') return;
		this.view = e.detail.view;
		if (e.detail.view === "gallery") { this.selectedAlbum = null; this.albumAssets = []; }
		this.lightboxAsset = null;
		this.render();
	};

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

	private getToken(): string | null {
		return localStorage.getItem('encryptid_token');
	}

	private authHeaders(): Record<string, string> {
		const token = this.getToken();
		return token ? { 'Authorization': `Bearer ${token}` } : {};
	}

	private async authFetch(url: string, opts: RequestInit = {}): Promise<Response> {
		const h = { ...this.authHeaders(), ...(opts.headers as Record<string, string> || {}) };
		return fetch(url, { ...opts, headers: h });
	}

	private async resolveUserRole() {
		const token = this.getToken();
		if (!token) { this.userRole = 'viewer'; return; }
		try {
			const base = this.getApiBase();
			// Fetch setup status and role in parallel
			const [setupRes, accessRes] = await Promise.all([
				this.authFetch(`${base}/api/setup/status`),
				this.authFetch(`/api/space-access/${encodeURIComponent(this.space)}`),
			]);
			if (setupRes.ok) {
				const data = await setupRes.json();
				this.spaceEnabled = data.enabled;
			}
			if (accessRes.ok) {
				const data = await accessRes.json();
				this.userRole = data.role || 'viewer';
			} else {
				this.userRole = 'viewer';
			}
		} catch {
			this.userRole = 'viewer';
		}
	}

	private async loadSetupAndGallery() {
		this.loading = true;
		this.render();
		await this.resolveUserRole();
		await this.loadGallery();
	}

	private loadDemoData() {
		this.userRole = 'viewer';
		this.albums = [
			{ id: "demo-album-1", albumName: "Community Gathering", description: "Photos from our community events", assetCount: 12, albumThumbnailAssetId: null, updatedAt: "2026-02-15T10:00:00Z", shared: true },
			{ id: "demo-album-2", albumName: "Workshop Series", description: "Hands-on learning sessions", assetCount: 8, albumThumbnailAssetId: null, updatedAt: "2026-02-10T14:30:00Z", shared: true },
			{ id: "demo-album-3", albumName: "Nature Walks", description: "Exploring local ecosystems", assetCount: 15, albumThumbnailAssetId: null, updatedAt: "2026-02-20T09:15:00Z", shared: true },
		];
		this.assets = [
			{ id: "demo-asset-1", type: "IMAGE", originalFileName: "sunrise-over-commons.jpg", fileCreatedAt: "2026-02-25T06:30:00Z", exifInfo: { city: "Portland", country: "USA", make: "Fujifilm", model: "X-T5" } },
			{ id: "demo-asset-2", type: "IMAGE", originalFileName: "workshop-group-photo.jpg", fileCreatedAt: "2026-02-24T15:00:00Z", exifInfo: { city: "Portland", country: "USA" } },
			{ id: "demo-asset-3", type: "IMAGE", originalFileName: "mycelium-closeup.jpg", fileCreatedAt: "2026-02-23T11:20:00Z", exifInfo: { make: "Canon", model: "EOS R5" } },
			{ id: "demo-asset-4", type: "IMAGE", originalFileName: "community-garden.jpg", fileCreatedAt: "2026-02-22T09:45:00Z", exifInfo: { city: "Seattle", country: "USA" } },
			{ id: "demo-asset-5", type: "IMAGE", originalFileName: "maker-space-tools.jpg", fileCreatedAt: "2026-02-21T14:10:00Z", exifInfo: {} },
			{ id: "demo-asset-6", type: "IMAGE", originalFileName: "sunset-gathering.jpg", fileCreatedAt: "2026-02-20T18:30:00Z", exifInfo: { city: "Vancouver", country: "Canada", make: "Sony", model: "A7IV" } },
			{ id: "demo-asset-7", type: "IMAGE", originalFileName: "seed-library.jpg", fileCreatedAt: "2026-02-19T10:00:00Z", exifInfo: {} },
			{ id: "demo-asset-8", type: "IMAGE", originalFileName: "potluck-spread.jpg", fileCreatedAt: "2026-02-18T12:00:00Z", exifInfo: { city: "Portland", country: "USA" } },
		];
		this.render();
	}

	private getDemoAssetMeta(id: string): { width: number; height: number; color: string } {
		const meta: Record<string, { width: number; height: number; color: string }> = {
			"demo-asset-1": { width: 4000, height: 2667, color: "#f59e0b" },
			"demo-asset-2": { width: 3200, height: 2400, color: "#6366f1" },
			"demo-asset-3": { width: 2400, height: 2400, color: "#22c55e" },
			"demo-asset-4": { width: 3600, height: 2400, color: "#10b981" },
			"demo-asset-5": { width: 2800, height: 1867, color: "#8b5cf6" },
			"demo-asset-6": { width: 4000, height: 2667, color: "#ef4444" },
			"demo-asset-7": { width: 2000, height: 2000, color: "#14b8a6" },
			"demo-asset-8": { width: 3200, height: 2133, color: "#f97316" },
		};
		return meta[id] || { width: 2000, height: 2000, color: "#64748b" };
	}

	private getDemoAlbumColor(id: string): string {
		const colors: Record<string, string> = {
			"demo-album-1": "#6366f1",
			"demo-album-2": "#22c55e",
			"demo-album-3": "#f59e0b",
		};
		return colors[id] || "#64748b";
	}

	private getDemoAlbumAssets(albumId: string): Asset[] {
		if (albumId === "demo-album-1") return this.assets.slice(0, 6);
		if (albumId === "demo-album-2") return this.assets.slice(2, 6);
		if (albumId === "demo-album-3") return this.assets.slice(0, 8);
		return [];
	}

	private isDemo(): boolean {
		return this.space === "demo" || this.showingSampleData;
	}

	private getApiBase(): string {
		const path = window.location.pathname;
		const match = path.match(/^(\/[^/]+)?\/rphotos/);
		return match ? match[0] : "";
	}

	private getImmichUrl(): string {
		return `${this.getApiBase()}/album`;
	}

	private getAlbumName(album: Album | SpaceAlbumInfo): string {
		return 'albumName' in album ? album.albumName : album.name;
	}

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

		const base = this.getApiBase();
		try {
			const [albumsRes, assetsRes] = await Promise.all([
				this.authFetch(`${base}/api/albums`),
				this.authFetch(`${base}/api/assets?size=24`),
			]);
			const albumsData = await albumsRes.json();
			const assetsData = await assetsRes.json();
			this.albums = albumsData.albums || [];
			this.assets = assetsData.assets || [];
		} catch {
			// Fall through to empty check below
		}

		if (this.albums.length === 0 && this.assets.length === 0 && !this.spaceEnabled) {
			this.showingSampleData = true;
			this.loading = false;
			this.loadDemoData();
			return;
		}

		this.loading = false;
		this.render();
	}

	private async loadAlbum(album: Album | SpaceAlbumInfo) {
		this.selectedAlbum = album;
		this.view = "album";

		if (this.isDemo()) {
			this.albumAssets = this.getDemoAlbumAssets(album.id);
			this.render();
			return;
		}

		this.loading = true;
		this.render();

		try {
			const base = this.getApiBase();
			const res = await this.authFetch(`${base}/api/albums/${album.id}`);
			const data = await res.json();
			this.albumAssets = data.assets || [];
		} catch {
			this.error = "Failed to load album";
		}

		this.loading = false;
		this.render();
	}

	private openLightbox(asset: Asset) {
		this.lightboxAsset = asset;
		this.view = "lightbox";
		this.render();
	}

	private closeLightbox() {
		this.lightboxAsset = null;
		this.view = this.selectedAlbum ? "album" : "gallery";
		this.render();
	}

	private goBack() {
		const prev = this._history.back();
		if (!prev) return;
		this.view = prev.view;
		if (prev.view === "gallery") {
			this.selectedAlbum = null;
			this.albumAssets = [];
		}
		this.lightboxAsset = null;
		this.render();
	}

	private thumbUrl(assetId: string): string {
		const base = this.getApiBase();
		return `${base}/api/assets/${assetId}/thumbnail`;
	}

	private originalUrl(assetId: string): string {
		const base = this.getApiBase();
		return `${base}/api/assets/${assetId}/original`;
	}

	private formatDate(d: string): string {
		return new Date(d).toLocaleDateString(undefined, {
			year: "numeric",
			month: "short",
			day: "numeric",
		});
	}

	// ── Upload ──

	private async handleUpload() {
		const input = document.createElement('input');
		input.type = 'file';
		input.accept = 'image/*';
		input.multiple = true;
		input.onchange = async () => {
			const files = input.files;
			if (!files || files.length === 0) return;

			this.uploading = true;
			this.error = "";
			this.render();

			const base = this.getApiBase();
			let successCount = 0;
			for (const file of Array.from(files)) {
				try {
					const formData = new FormData();
					formData.append('file', file);
					const res = await this.authFetch(`${base}/api/upload`, {
						method: 'POST',
						body: formData,
					});
					if (res.ok) successCount++;
					else {
						const data = await res.json().catch(() => ({}));
						this.error = data.error || `Upload failed (${res.status})`;
					}
				} catch (err: any) {
					this.error = `Upload failed: ${err.message}`;
				}
			}

			this.uploading = false;
			if (successCount > 0) {
				await this.loadGallery();
			} else {
				this.render();
			}
		};
		input.click();
	}

	// ── Delete asset ──

	private async handleDeleteAsset(assetId: string) {
		if (!confirm('Delete this photo? This cannot be undone.')) return;

		const base = this.getApiBase();
		try {
			const res = await this.authFetch(`${base}/api/assets/${assetId}`, { method: 'DELETE' });
			if (!res.ok) {
				const data = await res.json().catch(() => ({}));
				this.error = data.error || 'Delete failed';
				this.render();
				return;
			}
			// Remove from local state and go back
			this.assets = this.assets.filter(a => a.id !== assetId);
			this.albumAssets = this.albumAssets.filter(a => a.id !== assetId);
			this.goBack();
		} catch (err: any) {
			this.error = `Delete failed: ${err.message}`;
			this.render();
		}
	}

	// ── Setup ──

	private async handleEnableRPhotos() {
		const base = this.getApiBase();
		try {
			const res = await this.authFetch(`${base}/api/setup`, { method: 'POST' });
			if (res.ok) {
				this.spaceEnabled = true;
				await this.loadGallery();
			} else {
				const data = await res.json().catch(() => ({}));
				this.error = data.error || 'Setup failed';
				this.render();
			}
		} catch (err: any) {
			this.error = `Setup failed: ${err.message}`;
			this.render();
		}
	}

	private render() {
		const canUpload = roleAtLeast(this.userRole, 'member') && this.spaceEnabled;
		const canDelete = roleAtLeast(this.userRole, 'moderator');
		const isAdmin = roleAtLeast(this.userRole, 'admin');

		this.shadow.innerHTML = `
		<style>
			:host { display: block; font-family: system-ui, -apple-system, sans-serif; color: #e2e8f0; }
			* { box-sizing: border-box; }

			.rapp-nav { display: flex; align-items: center; gap: 8px; margin-bottom: 16px; min-height: 36px; }
			.rapp-nav__back { padding: 4px 10px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.1); background: transparent; color: #94a3b8; cursor: pointer; font-size: 13px; transition: color 0.15s, border-color 0.15s; }
			.rapp-nav__back:hover { color: #e2e8f0; border-color: rgba(255,255,255,0.2); }
			.rapp-nav__title { font-size: 15px; font-weight: 600; color: #e2e8f0; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
			.rapp-nav__actions { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
			.rapp-nav__btn { padding: 6px 14px; border-radius: 6px; border: none; background: #4f46e5; color: #fff; font-weight: 600; cursor: pointer; font-size: 13px; text-decoration: none; display: inline-flex; align-items: center; gap: 6px; }
			.rapp-nav__btn:hover { background: #6366f1; }
			.rapp-nav__btn--secondary { background: transparent; border: 1px solid rgba(255,255,255,0.15); color: #94a3b8; }
			.rapp-nav__btn--secondary:hover { color: #e2e8f0; border-color: rgba(255,255,255,0.3); }
			.rapp-nav__btn--danger { background: #dc2626; }
			.rapp-nav__btn--danger:hover { background: #ef4444; }
			.rapp-nav__btn:disabled { opacity: 0.5; cursor: not-allowed; }

			.albums-section { margin-bottom: 2rem; }
			.section-title { font-size: 13px; font-weight: 600; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 12px; }

			.albums-grid {
				display: grid;
				grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
				gap: 12px;
			}

			.album-card {
				background: #1e293b;
				border: 1px solid #334155;
				border-radius: 12px;
				overflow: hidden;
				cursor: pointer;
				transition: border-color 0.2s, transform 0.15s;
			}
			.album-card:hover { border-color: #f9a8d4; transform: translateY(-2px); }

			.album-thumb {
				aspect-ratio: 16/10;
				background: #0f172a;
				display: flex;
				align-items: center;
				justify-content: center;
				overflow: hidden;
			}
			.album-thumb img { width: 100%; height: 100%; object-fit: cover; }
			.album-thumb-empty { font-size: 2.5rem; opacity: 0.3; }

			.album-info { padding: 10px 12px; }
			.album-name { font-size: 14px; font-weight: 600; color: #f1f5f9; margin-bottom: 2px; }
			.album-meta { font-size: 12px; color: #64748b; }

			.photo-grid {
				display: grid;
				grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
				gap: 4px;
			}

			.photo-cell {
				aspect-ratio: 1;
				overflow: hidden;
				cursor: pointer;
				border-radius: 4px;
				background: #0f172a;
				transition: opacity 0.15s;
			}
			.photo-cell:hover { opacity: 0.85; }
			.photo-cell img { width: 100%; height: 100%; object-fit: cover; }

			.lightbox {
				position: fixed;
				inset: 0;
				z-index: 9999;
				background: rgba(0,0,0,0.92);
				display: flex;
				align-items: center;
				justify-content: center;
				flex-direction: column;
				gap: 12px;
			}
			.lightbox img {
				max-width: 90vw;
				max-height: 80vh;
				object-fit: contain;
				border-radius: 8px;
			}
			.lightbox-close {
				position: absolute;
				top: 16px;
				right: 16px;
				background: rgba(255,255,255,0.1);
				border: none;
				color: #fff;
				font-size: 24px;
				width: 44px;
				height: 44px;
				border-radius: 50%;
				cursor: pointer;
				display: flex;
				align-items: center;
				justify-content: center;
			}
			.lightbox-close:hover { background: rgba(255,255,255,0.2); }
			.lightbox-actions {
				position: absolute;
				top: 16px;
				left: 16px;
				display: flex;
				gap: 8px;
			}
			.lightbox-delete {
				background: rgba(220,38,38,0.8);
				border: none;
				color: #fff;
				font-size: 14px;
				padding: 8px 16px;
				border-radius: 6px;
				cursor: pointer;
				font-weight: 600;
			}
			.lightbox-delete:hover { background: rgba(239,68,68,0.9); }
			.lightbox-info {
				text-align: center;
				color: #94a3b8;
				font-size: 13px;
			}

			.empty { text-align: center; color: #64748b; padding: 3rem 1rem; }
			.empty-icon { font-size: 3rem; margin-bottom: 1rem; opacity: 0.4; }
			.empty h3 { color: #94a3b8; margin: 0 0 0.5rem; }
			.empty p { margin: 0 0 1.5rem; font-size: 0.9rem; }

			.setup-card {
				text-align: center;
				padding: 3rem 1.5rem;
				background: #1e293b;
				border: 1px solid #334155;
				border-radius: 12px;
				margin: 1rem 0;
			}
			.setup-card h3 { color: #e2e8f0; margin: 0 0 0.5rem; }
			.setup-card p { color: #64748b; margin: 0 0 1.5rem; font-size: 0.9rem; }

			.loading { text-align: center; color: #64748b; padding: 3rem; }
			.error { text-align: center; color: #f87171; padding: 1.5rem; background: rgba(248,113,113,0.08); border-radius: 8px; margin-bottom: 16px; font-size: 14px; }

			.demo-thumb {
				width: 100%; height: 100%;
				display: flex; align-items: center; justify-content: center;
				font-size: 11px; font-weight: 600; color: rgba(255,255,255,0.85);
				text-align: center; padding: 8px;
				text-shadow: 0 1px 3px rgba(0,0,0,0.4);
				word-break: break-word; line-height: 1.3;
			}
			.demo-lightbox-img {
				width: 80vw; max-width: 900px; aspect-ratio: 3/2;
				display: flex; align-items: center; justify-content: center;
				border-radius: 8px;
				font-size: 20px; font-weight: 600; color: rgba(255,255,255,0.9);
				text-shadow: 0 2px 6px rgba(0,0,0,0.5);
			}

			.sample-banner { padding: 8px 16px; background: rgba(99,102,241,0.12); border: 1px solid rgba(99,102,241,0.25); border-radius: 8px; color: #a5b4fc; font-size: 13px; text-align: center; margin-bottom: 12px; }

			@media (max-width: 480px) {
				.albums-grid { grid-template-columns: 1fr; }
				.photo-grid { grid-template-columns: repeat(2, 1fr); }
			}
		</style>

		${this.error ? `<div class="error">${this.esc(this.error)}</div>` : ""}
		${this.loading ? '<div class="loading">Loading photos...</div>' : ""}
		${this.uploading ? '<div class="loading">Uploading...</div>' : ""}
		${!this.loading && !this.uploading ? this.renderView() : ""}
		${this.view === "lightbox" && this.lightboxAsset ? this.renderLightbox() : ""}
		`;

		this.attachListeners();

		// Make photo cells and album cards draggable for calendar reminders
		makeDraggableAll(this.shadow, ".photo-cell[data-asset-id]", (el) => {
			const img = el.querySelector("img");
			const title = img?.alt || "Photo";
			const id = el.dataset.assetId || "";
			return { title, module: "rphotos", entityId: id, label: "Photo", color: "#ec4899" };
		});
		makeDraggableAll(this.shadow, ".album-card[data-album-id]", (el) => {
			const title = el.querySelector(".album-name")?.textContent || "Album";
			const id = el.dataset.albumId || "";
			return { title, module: "rphotos", entityId: id, label: "Album", color: "#ec4899" };
		});

		this._tour.renderOverlay();
	}

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

	private renderView(): string {
		// Not enabled — show setup or placeholder
		if (!this.isDemo() && !this.spaceEnabled) {
			return this.renderSetupPrompt();
		}
		if (this.view === "album" && this.selectedAlbum) return this.renderAlbum();
		return this.renderGalleryView();
	}

	private renderSetupPrompt(): string {
		if (roleAtLeast(this.userRole, 'admin')) {
			return `
				<div class="setup-card">
					<div style="font-size:3rem;margin-bottom:1rem;opacity:0.4">📸</div>
					<h3>Enable rPhotos for this space</h3>
					<p>Create a dedicated photo library for this space, backed by Immich. Members will be able to upload and browse photos.</p>
					<button class="rapp-nav__btn" id="btn-enable">Enable rPhotos</button>
				</div>
			`;
		}
		return `
			<div class="empty">
				<div class="empty-icon">📸</div>
				<h3>rPhotos isn't enabled yet</h3>
				<p>Ask a space admin to enable the photo library for this space.</p>
			</div>
		`;
	}

	private renderGalleryView(): string {
		const hasContent = this.albums.length > 0 || this.assets.length > 0;
		const canUpload = roleAtLeast(this.userRole, 'member') && this.spaceEnabled && !this.isDemo();
		const isAdmin = roleAtLeast(this.userRole, 'admin');

		return `
			<div class="rapp-nav">
				<span class="rapp-nav__title">Photos</span>
				<div class="rapp-nav__actions">
					${canUpload ? '<button class="rapp-nav__btn" id="btn-upload">Upload</button>' : ''}
					${isAdmin ? `<a class="rapp-nav__btn rapp-nav__btn--secondary" href="${this.getImmichUrl()}">Open Immich</a>` : ''}
					<button class="rapp-nav__btn rapp-nav__btn--secondary" id="btn-tour" style="font-size:0.78rem;padding:4px 10px;opacity:0.7">Tour</button>
				</div>
			</div>

			${this.showingSampleData ? '<div class="sample-banner">Showing sample data — connect Immich to see your photos</div>' : ''}

			${!hasContent ? `
				<div class="empty">
					<div class="empty-icon">📸</div>
					<h3>No photos yet</h3>
					<p>${canUpload ? 'Upload the first photo to get started!' : 'Photos uploaded by members will appear here.'}</p>
					${canUpload ? '<button class="rapp-nav__btn" id="btn-upload-empty">Upload Photos</button>' : ''}
				</div>
			` : ""}

			${this.albums.length > 0 ? `
				<div class="albums-section">
					<div class="section-title">Albums</div>
					<div class="albums-grid">
						${this.albums.map((a) => {
							const name = this.getAlbumName(a);
							const isImmichAlbum = 'albumName' in a;
							return `
							<div class="album-card" data-album-id="${a.id}" data-collab-id="album:${a.id}">
								<div class="album-thumb">
									${this.isDemo()
										? `<div class="demo-thumb" style="background:${this.getDemoAlbumColor(a.id)}">${this.esc(name)}</div>`
										: isImmichAlbum && (a as Album).albumThumbnailAssetId
											? `<img src="${this.thumbUrl((a as Album).albumThumbnailAssetId!)}" alt="${this.esc(name)}" loading="lazy">`
											: '<span class="album-thumb-empty">📷</span>'}
								</div>
								<div class="album-info">
									<div class="album-name">${this.esc(name)}</div>
									${isImmichAlbum ? `<div class="album-meta">${(a as Album).assetCount} photo${(a as Album).assetCount !== 1 ? "s" : ""}</div>` : ''}
								</div>
							</div>
						`;}).join("")}
					</div>
				</div>
			` : ""}

			${this.assets.length > 0 ? `
				<div class="section-title">Recent Photos</div>
				<div class="photo-grid">
					${this.assets.map((a) => `
						<div class="photo-cell" data-asset-id="${a.id}" data-collab-id="photo:${a.id}">
							${this.isDemo()
								? `<div class="demo-thumb" style="background:${this.getDemoAssetMeta(a.id).color}">${this.esc(a.originalFileName.replace(/\.[^.]+$/, "").replace(/-/g, " "))}</div>`
								: `<img src="${this.thumbUrl(a.id)}" alt="${this.esc(a.originalFileName)}" loading="lazy">`}
						</div>
					`).join("")}
				</div>
			` : ""}
		`;
	}

	private renderAlbum(): string {
		const album = this.selectedAlbum!;
		const name = this.getAlbumName(album);
		const isAdmin = roleAtLeast(this.userRole, 'admin');
		return `
			<div class="rapp-nav">
				${this._history.canGoBack ? '<button class="rapp-nav__back" data-back="gallery">\u2190 Photos</button>' : ''}
				<span class="rapp-nav__title">${this.esc(name)}</span>
				<div class="rapp-nav__actions">
					${isAdmin ? `<a class="rapp-nav__btn rapp-nav__btn--secondary" href="${this.getImmichUrl()}">Open in Immich</a>` : ''}
				</div>
			</div>

			${this.albumAssets.length === 0 ? `
				<div class="empty">
					<div class="empty-icon">📷</div>
					<h3>Album is empty</h3>
					<p>Upload photos to populate this album.</p>
				</div>
			` : `
				<div class="photo-grid">
					${this.albumAssets.map((a) => `
						<div class="photo-cell" data-asset-id="${a.id}" data-collab-id="photo:${a.id}">
							${this.isDemo()
								? `<div class="demo-thumb" style="background:${this.getDemoAssetMeta(a.id).color}">${this.esc(a.originalFileName.replace(/\.[^.]+$/, "").replace(/-/g, " "))}</div>`
								: `<img src="${this.thumbUrl(a.id)}" alt="${this.esc(a.originalFileName)}" loading="lazy">`}
						</div>
					`).join("")}
				</div>
			`}
		`;
	}

	private renderLightbox(): string {
		const asset = this.lightboxAsset!;
		const info = asset.exifInfo;
		const location = [info?.city, info?.country].filter(Boolean).join(", ");
		const camera = [info?.make, info?.model].filter(Boolean).join(" ");
		const canDelete = roleAtLeast(this.userRole, 'moderator') && !this.isDemo();

		const demoMeta = this.isDemo() ? this.getDemoAssetMeta(asset.id) : null;
		const displayName = asset.originalFileName.replace(/\.[^.]+$/, "").replace(/-/g, " ");

		return `
			<div class="lightbox" data-lightbox>
				<button class="lightbox-close" data-close-lightbox>✕</button>
				${canDelete ? `
					<div class="lightbox-actions">
						<button class="lightbox-delete" data-delete-asset="${asset.id}">Delete</button>
					</div>
				` : ''}
				${demoMeta
					? `<div class="demo-lightbox-img" style="background:${demoMeta.color}">${this.esc(displayName)}</div>`
					: `<img src="${this.originalUrl(asset.id)}" alt="${this.esc(asset.originalFileName)}">`}
				<div class="lightbox-info">
					${asset.originalFileName}${demoMeta ? ` &middot; ${demoMeta.width}x${demoMeta.height}` : ""}
					${location ? ` &middot; ${this.esc(location)}` : ""}
					${camera ? ` &middot; ${this.esc(camera)}` : ""}
					&middot; ${this.formatDate(asset.fileCreatedAt)}
				</div>
			</div>
		`;
	}

	private attachListeners() {
		this.shadow.querySelector("#btn-tour")?.addEventListener("click", () => this.startTour());
		this.shadow.querySelector("#btn-upload")?.addEventListener("click", () => this.handleUpload());
		this.shadow.querySelector("#btn-upload-empty")?.addEventListener("click", () => this.handleUpload());
		this.shadow.querySelector("#btn-enable")?.addEventListener("click", () => this.handleEnableRPhotos());

		// Delete button in lightbox
		this.shadow.querySelectorAll("[data-delete-asset]").forEach((el) => {
			el.addEventListener("click", (e) => {
				e.stopPropagation();
				const assetId = (el as HTMLElement).dataset.deleteAsset!;
				this.handleDeleteAsset(assetId);
			});
		});

		// Album cards
		this.shadow.querySelectorAll("[data-album-id]").forEach((el) => {
			el.addEventListener("click", () => {
				const id = (el as HTMLElement).dataset.albumId!;
				const album = this.albums.find((a) => a.id === id);
				if (album) {
					this._history.push(this.view);
					this._history.push("album", { albumId: id });
					this.loadAlbum(album);
				}
			});
		});

		// Photo cells
		this.shadow.querySelectorAll("[data-asset-id]").forEach((el) => {
			el.addEventListener("click", () => {
				const id = (el as HTMLElement).dataset.assetId!;
				const assets = this.view === "album" ? this.albumAssets : this.assets;
				const asset = assets.find((a) => a.id === id);
				if (asset) {
					this._history.push(this.view);
					this._history.push("lightbox", { assetId: id });
					this.openLightbox(asset);
				}
			});
		});

		// Back button
		this.shadow.querySelectorAll("[data-back]").forEach((el) => {
			el.addEventListener("click", () => this.goBack());
		});

		// Lightbox close
		this.shadow.querySelector("[data-close-lightbox]")?.addEventListener("click", () => this.goBack());
		this.shadow.querySelector("[data-lightbox]")?.addEventListener("click", (e) => {
			if ((e.target as HTMLElement).matches("[data-lightbox]")) this.goBack();
		});
	}

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

customElements.define("folk-photo-gallery", FolkPhotoGallery);
