/**
 * folk-spider-3d.ts — FolkShape web component for 3D stacked spider plots.
 *
 * Uses CSS 3D transforms (perspective + rotateX/Y on stacked SVG layers)
 * to render overlapping radar plots with aggregate z-dimension height.
 */

import { FolkShape } from "./folk-shape";
import { css, html } from "./tags";
import { getModuleApiBase } from "../shared/url-helpers";
import {
	computeSpider3D,
	computeRadarPolygon,
	membranePreset,
	DEMO_CONFIG,
	type Spider3DAxis,
	type Spider3DConfig,
	type Spider3DDataset,
	type Spider3DResult,
	type Spider3DLayer,
	type Spider3DOverlapRegion,
} from "./spider-3d";
import type { FlowKind } from "./layer-types";

const styles = css`
	:host {
		background: var(--rs-bg-surface, #0f172a);
		color: var(--rs-text-primary, #e2e8f0);
		border-radius: 8px;
		box-shadow: var(--rs-shadow-md, 0 2px 12px rgba(0, 0, 0, 0.3));
		min-width: 400px;
		min-height: 420px;
		overflow: hidden;
	}

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

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

	.header-actions button.active {
		background: rgba(255, 255, 255, 0.3);
	}

	.mode-toggle {
		display: flex;
		gap: 2px;
		background: rgba(255, 255, 255, 0.15);
		border-radius: 4px;
		padding: 1px;
	}

	.mode-btn {
		background: transparent;
		border: none;
		color: rgba(255, 255, 255, 0.7);
		cursor: pointer;
		padding: 2px 8px;
		border-radius: 3px;
		font-size: 10px;
		font-weight: 500;
	}

	.mode-btn.active {
		background: rgba(255, 255, 255, 0.3);
		color: white;
	}

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

	.scene {
		flex: 1;
		display: flex;
		align-items: center;
		justify-content: center;
		perspective: 800px;
		perspective-origin: 50% 50%;
		cursor: grab;
		min-height: 250px;
		overflow: hidden;
	}

	.scene:active {
		cursor: grabbing;
	}

	.stage {
		position: relative;
		transform-style: preserve-3d;
		transition: transform 0.1s ease-out;
		width: 280px;
		height: 280px;
	}

	.layer {
		position: absolute;
		top: 0;
		left: 0;
		width: 100%;
		height: 100%;
		transform-style: preserve-3d;
		backface-visibility: hidden;
		pointer-events: none;
	}

	.layer.interactive {
		pointer-events: auto;
	}

	.layer.overlap polygon {
		filter: drop-shadow(0 0 4px rgba(255, 255, 255, 0.3));
	}

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

	.legend-item {
		display: flex;
		align-items: center;
		gap: 4px;
		font-size: 10px;
		color: #94a3b8;
		cursor: pointer;
		padding: 2px 6px;
		border-radius: 4px;
		transition: all 0.15s;
		user-select: none;
	}

	.legend-item:hover {
		background: rgba(255, 255, 255, 0.08);
	}

	.legend-item.hidden {
		opacity: 0.4;
	}

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

	.tooltip {
		display: none;
		position: absolute;
		bottom: 8px;
		left: 50%;
		transform: translateX(-50%);
		background: rgba(15, 23, 42, 0.95);
		color: #e2e8f0;
		padding: 6px 10px;
		border-radius: 6px;
		font-size: 10px;
		white-space: nowrap;
		border: 1px solid rgba(255, 255, 255, 0.15);
		pointer-events: none;
		z-index: 10;
	}

	.tooltip.visible {
		display: block;
	}

	.info-bar {
		display: flex;
		justify-content: space-between;
		align-items: center;
		padding: 4px 12px;
		font-size: 10px;
		color: #64748b;
		border-top: 1px solid rgba(255, 255, 255, 0.05);
	}
`;

declare global {
	interface HTMLElementTagNameMap {
		"folk-spider-3d": FolkSpider3D;
	}
}

function escapeHtml(text: string): string {
	return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}

/** Blend two hex colors by averaging RGB channels */
function blendColors(colors: string[]): string {
	if (colors.length === 0) return "#ffffff";
	if (colors.length === 1) return colors[0];

	let r = 0, g = 0, b = 0;
	for (const c of colors) {
		const hex = c.replace("#", "");
		r += parseInt(hex.substring(0, 2), 16);
		g += parseInt(hex.substring(2, 4), 16);
		b += parseInt(hex.substring(4, 6), 16);
	}
	r = Math.round(r / colors.length);
	g = Math.round(g / colors.length);
	b = Math.round(b / colors.length);
	return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
}

export class FolkSpider3D extends FolkShape {
	static override tagName = "folk-spider-3d";

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

	// ── Properties ──

	#title = "Spider 3D";
	#axes: Spider3DAxis[] = [];
	#datasets: Spider3DDataset[] = [];
	#tiltX = 35;
	#tiltY = -15;
	#layerSpacing = 8;
	#showOverlapHeight = true;
	#mode: "datasets" | "membrane" = "datasets";
	#space = "";

	// ── Internal state ──

	#hiddenDatasets = new Set<string>();
	#isDragging = false;
	#lastPointerX = 0;
	#lastPointerY = 0;
	#result: Spider3DResult | null = null;

	// ── DOM refs ──

	#wrapperEl: HTMLElement | null = null;
	#sceneEl: HTMLElement | null = null;
	#stageEl: HTMLElement | null = null;
	#legendEl: HTMLElement | null = null;
	#tooltipEl: HTMLElement | null = null;
	#infoEl: HTMLElement | null = null;

	// ── Accessors ──

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

	get axes() { return this.#axes; }
	set axes(v: Spider3DAxis[]) { this.#axes = v; this.#recompute(); }

	get datasets() { return this.#datasets; }
	set datasets(v: Spider3DDataset[]) { this.#datasets = v; this.#recompute(); }

	get tiltX() { return this.#tiltX; }
	set tiltX(v: number) { this.#tiltX = v; this.#updateTilt(); }

	get tiltY() { return this.#tiltY; }
	set tiltY(v: number) { this.#tiltY = v; this.#updateTilt(); }

	get layerSpacing() { return this.#layerSpacing; }
	set layerSpacing(v: number) { this.#layerSpacing = v; this.#recompute(); }

	get showOverlapHeight() { return this.#showOverlapHeight; }
	set showOverlapHeight(v: boolean) { this.#showOverlapHeight = v; this.#recompute(); }

	get mode() { return this.#mode; }
	set mode(v: "datasets" | "membrane") {
		this.#mode = v;
		if (v === "membrane") this.#loadMembrane();
		this.#render();
	}

	get space() { return this.#space; }
	set space(v: string) {
		this.#space = v;
		if (this.#mode === "membrane") this.#loadMembrane();
	}

	// ── Lifecycle ──

	override createRenderRoot() {
		const root = super.createRenderRoot();

		const wrapper = document.createElement("div");
		wrapper.style.cssText = "position:relative;height:100%;";
		wrapper.innerHTML = html`
			<div class="header">
				<span class="header-title">
					<span>&#x1F578;</span>
					<span class="title-text">${escapeHtml(this.#title)}</span>
				</span>
				<div class="header-actions">
					<div class="mode-toggle">
						<button class="mode-btn mode-datasets active">Datasets</button>
						<button class="mode-btn mode-membrane">Membrane</button>
					</div>
					<button class="tilt-reset" title="Reset tilt">&#x21BA;</button>
					<button class="close-btn" title="Close">&times;</button>
				</div>
			</div>
			<div class="body">
				<div class="scene">
					<div class="stage"></div>
				</div>
				<div class="legend"></div>
				<div class="info-bar">
					<span class="info-text"></span>
					<span class="overlap-count"></span>
				</div>
				<div class="tooltip"></div>
			</div>
		`;

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

		this.#wrapperEl = wrapper;
		this.#sceneEl = wrapper.querySelector(".scene") as HTMLElement;
		this.#stageEl = wrapper.querySelector(".stage") as HTMLElement;
		this.#legendEl = wrapper.querySelector(".legend") as HTMLElement;
		this.#tooltipEl = wrapper.querySelector(".tooltip") as HTMLElement;
		this.#infoEl = wrapper.querySelector(".info-bar") as HTMLElement;

		// Mode toggle
		const modeDatasets = wrapper.querySelector(".mode-datasets") as HTMLButtonElement;
		const modeMembrane = wrapper.querySelector(".mode-membrane") as HTMLButtonElement;

		modeDatasets.addEventListener("click", (e) => {
			e.stopPropagation();
			this.#mode = "datasets";
			modeDatasets.classList.add("active");
			modeMembrane.classList.remove("active");
			this.#recompute();
			this.dispatchEvent(new CustomEvent("content-change"));
		});

		modeMembrane.addEventListener("click", (e) => {
			e.stopPropagation();
			this.#mode = "membrane";
			modeMembrane.classList.add("active");
			modeDatasets.classList.remove("active");
			this.#loadMembrane();
			this.dispatchEvent(new CustomEvent("content-change"));
		});

		// Tilt reset
		wrapper.querySelector(".tilt-reset")!.addEventListener("click", (e) => {
			e.stopPropagation();
			this.#tiltX = 35;
			this.#tiltY = -15;
			this.#updateTilt();
			this.dispatchEvent(new CustomEvent("content-change"));
		});

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

		// Interactive orbit (pointer drag on scene)
		this.#sceneEl.addEventListener("pointerdown", (e) => {
			// Only orbit with left button on the scene background
			if (e.button !== 0) return;
			const target = e.target as HTMLElement;
			if (target !== this.#sceneEl && target !== this.#stageEl) return;
			e.stopPropagation();
			this.#isDragging = true;
			this.#lastPointerX = e.clientX;
			this.#lastPointerY = e.clientY;
			this.#sceneEl!.setPointerCapture(e.pointerId);
		});

		this.#sceneEl.addEventListener("pointermove", (e) => {
			if (!this.#isDragging) return;
			e.stopPropagation();
			const dx = e.clientX - this.#lastPointerX;
			const dy = e.clientY - this.#lastPointerY;
			this.#lastPointerX = e.clientX;
			this.#lastPointerY = e.clientY;

			this.#tiltY += dx * 0.5;
			this.#tiltX = Math.max(0, Math.min(80, this.#tiltX - dy * 0.5));
			this.#updateTilt();
		});

		this.#sceneEl.addEventListener("pointerup", (e) => {
			if (!this.#isDragging) return;
			e.stopPropagation();
			this.#isDragging = false;
			this.#sceneEl!.releasePointerCapture(e.pointerId);
			this.dispatchEvent(new CustomEvent("content-change"));
		});

		this.#sceneEl.addEventListener("lostpointercapture", () => {
			this.#isDragging = false;
		});

		// Load initial data
		if (this.#axes.length === 0 && this.#datasets.length === 0) {
			this.#axes = DEMO_CONFIG.axes;
			this.#datasets = DEMO_CONFIG.datasets;
		}

		this.#recompute();
		return root;
	}

	// ── Computation ──

	#recompute() {
		const visibleDatasets = this.#datasets.filter(
			(ds) => !this.#hiddenDatasets.has(ds.id),
		);

		const config: Spider3DConfig = {
			axes: this.#axes,
			datasets: visibleDatasets,
			resolution: 36,
		};

		this.#result = computeSpider3D(config, 140, 140, 110);
		this.#render();
	}

	#updateTilt() {
		if (!this.#stageEl) return;
		this.#stageEl.style.transform =
			`rotateX(${this.#tiltX}deg) rotateY(${this.#tiltY}deg)`;
	}

	// ── Membrane mode ──

	async #loadMembrane() {
		if (!this.#space) {
			// Fall back to demo data
			this.#axes = DEMO_CONFIG.axes;
			this.#datasets = DEMO_CONFIG.datasets;
			this.#recompute();
			return;
		}

		try {
			const token = localStorage.getItem("rspace-token") || "";
			const res = await fetch(`${getModuleApiBase("rspace")}/connections`, {
				headers: token ? { Authorization: `Bearer ${token}` } : {},
			});
			if (!res.ok) throw new Error(`HTTP ${res.status}`);
			const connections = await res.json();
			const preset = membranePreset(connections);
			this.#axes = preset.axes;
			this.#datasets = preset.datasets;
			this.#recompute();
		} catch {
			// Fall back to demo data
			this.#axes = DEMO_CONFIG.axes;
			this.#datasets = DEMO_CONFIG.datasets;
			this.#recompute();
		}
	}

	// ── Rendering ──

	#render() {
		this.#renderStage();
		this.#renderLegend();
		this.#renderInfo();
	}

	#renderStage() {
		if (!this.#stageEl || !this.#result) return;

		const { layers, overlapRegions, maxHeight } = this.#result;
		const CX = 140, CY = 140, R = 110;
		const n = this.#axes.length;
		if (n < 3) {
			this.#stageEl.innerHTML = "";
			return;
		}

		const angleStep = (2 * Math.PI) / n;
		const RINGS = 5;

		let layersHtml = "";

		// ── Grid layer (z = 0) ──
		let gridSvg = `<svg viewBox="0 0 280 280" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:100%;">`;

		// Grid rings
		for (let ring = 1; ring <= RINGS; ring++) {
			const r = (R / RINGS) * ring;
			const pts = Array.from({ length: n }, (_, i) => {
				const angle = i * angleStep - Math.PI / 2;
				return `${CX + r * Math.cos(angle)},${CY + r * Math.sin(angle)}`;
			}).join(" ");
			gridSvg += `<polygon points="${pts}" fill="none" stroke="rgba(148,163,184,0.2)" stroke-width="${ring === RINGS ? 1.5 : 0.5}"/>`;
		}

		// Axis spokes + labels
		for (let i = 0; i < n; i++) {
			const angle = i * angleStep - Math.PI / 2;
			const ex = CX + R * Math.cos(angle);
			const ey = CY + R * Math.sin(angle);
			const lx = CX + (R + 18) * Math.cos(angle);
			const ly = CY + (R + 18) * Math.sin(angle);
			gridSvg += `<line x1="${CX}" y1="${CY}" x2="${ex}" y2="${ey}" stroke="rgba(148,163,184,0.15)" stroke-width="0.5"/>`;
			gridSvg += `<text x="${lx}" y="${ly}" text-anchor="middle" dominant-baseline="central" fill="#64748b" font-size="9" font-family="system-ui">${escapeHtml(this.#axes[i].label)}</text>`;
		}

		gridSvg += `</svg>`;
		layersHtml += `<div class="layer" style="transform:translateZ(0px)">${gridSvg}</div>`;

		// ── Dataset layers ──
		for (const layer of layers) {
			if (layer.vertices.length < 3) continue;
			const z = (layer.zIndex + 1) * this.#layerSpacing;
			const pts = layer.vertices.map((v) => `${v.x},${v.y}`).join(" ");

			let svg = `<svg viewBox="0 0 280 280" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:100%;">`;
			svg += `<polygon points="${pts}" fill="${layer.color}" fill-opacity="0.3" stroke="${layer.color}" stroke-width="1.5" stroke-opacity="0.7"/>`;

			// Vertex dots
			for (const v of layer.vertices) {
				svg += `<circle cx="${v.x}" cy="${v.y}" r="2.5" fill="${layer.color}" opacity="0.9"/>`;
			}

			svg += `</svg>`;
			layersHtml += `<div class="layer" style="transform:translateZ(${z}px)">${svg}</div>`;
		}

		// ── Overlap layers ──
		if (this.#showOverlapHeight && overlapRegions.length > 0) {
			const baseZ = (layers.length + 1) * this.#layerSpacing;

			for (const region of overlapRegions) {
				if (region.vertices.length < 3) continue;

				const colors = region.contributorIds.map((id) => {
					const ds = this.#datasets.find((d) => d.id === id);
					return ds?.color ?? "#ffffff";
				});
				const blended = blendColors(colors);

				// Stack multiple semi-transparent layers for taller overlap regions
				const overlapLayers = Math.min(region.height, 4);
				for (let ol = 0; ol < overlapLayers; ol++) {
					const z = baseZ + ol * (this.#layerSpacing * 0.6);
					const pts = region.vertices.map((v) => `${v.x},${v.y}`).join(" ");
					const opacity = 0.15 + ol * 0.05;

					let svg = `<svg viewBox="0 0 280 280" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:100%;">`;
					svg += `<polygon points="${pts}" fill="${blended}" fill-opacity="${opacity}" stroke="rgba(255,255,255,0.3)" stroke-width="0.5"/>`;
					svg += `</svg>`;
					layersHtml += `<div class="layer overlap" style="transform:translateZ(${z}px)">${svg}</div>`;
				}
			}
		}

		this.#stageEl.innerHTML = layersHtml;
		this.#updateTilt();
	}

	#renderLegend() {
		if (!this.#legendEl) return;

		this.#legendEl.innerHTML = this.#datasets
			.map((ds) => {
				const hidden = this.#hiddenDatasets.has(ds.id);
				return `<span class="legend-item ${hidden ? "hidden" : ""}" data-ds="${escapeHtml(ds.id)}">
					<span class="legend-dot" style="background:${ds.color}"></span>
					${escapeHtml(ds.label)}
				</span>`;
			})
			.join("");

		this.#legendEl.querySelectorAll(".legend-item").forEach((el) => {
			el.addEventListener("click", (e) => {
				e.stopPropagation();
				const dsId = (el as HTMLElement).dataset.ds!;
				if (this.#hiddenDatasets.has(dsId)) {
					this.#hiddenDatasets.delete(dsId);
				} else {
					this.#hiddenDatasets.add(dsId);
				}
				this.#recompute();
			});
		});
	}

	#renderInfo() {
		if (!this.#infoEl || !this.#result) return;

		const visibleCount = this.#datasets.length - this.#hiddenDatasets.size;
		const overlapCount = this.#result.overlapRegions.length;
		const infoText = this.#infoEl.querySelector(".info-text") as HTMLElement;
		const overlapEl = this.#infoEl.querySelector(".overlap-count") as HTMLElement;

		if (infoText) infoText.textContent = `${visibleCount} dataset${visibleCount !== 1 ? "s" : ""}`;
		if (overlapEl) overlapEl.textContent = overlapCount > 0
			? `${overlapCount} overlap region${overlapCount !== 1 ? "s" : ""}`
			: "";
	}

	// ── Serialization ──

	static override fromData(data: Record<string, any>): FolkSpider3D {
		const el = FolkShape.fromData(data) as FolkSpider3D;
		if (data.title !== undefined) el.title = data.title;
		if (data.axes !== undefined) el.axes = data.axes;
		if (data.datasets !== undefined) el.datasets = data.datasets;
		if (data.tiltX != null) el.tiltX = data.tiltX;
		if (data.tiltY != null) el.tiltY = data.tiltY;
		if (data.layerSpacing != null) el.layerSpacing = data.layerSpacing;
		if (data.showOverlapHeight != null) el.showOverlapHeight = data.showOverlapHeight;
		if (data.mode !== undefined) el.mode = data.mode;
		if (data.space !== undefined) el.space = data.space;
		return el;
	}

	override toJSON() {
		return {
			...super.toJSON(),
			type: "folk-spider-3d",
			title: this.#title,
			axes: this.#axes,
			datasets: this.#datasets,
			tiltX: this.#tiltX,
			tiltY: this.#tiltY,
			layerSpacing: this.#layerSpacing,
			showOverlapHeight: this.#showOverlapHeight,
			mode: this.#mode,
			space: this.#space,
		};
	}

	override applyData(data: Record<string, any>): void {
		super.applyData(data);
		if (data.title !== undefined && this.#title !== data.title) this.title = data.title;
		if (data.axes !== undefined && JSON.stringify(this.#axes) !== JSON.stringify(data.axes)) this.axes = data.axes;
		if (data.datasets !== undefined && JSON.stringify(this.#datasets) !== JSON.stringify(data.datasets)) this.datasets = data.datasets;
		if (data.tiltX != null && this.#tiltX !== data.tiltX) this.tiltX = data.tiltX;
		if (data.tiltY != null && this.#tiltY !== data.tiltY) this.tiltY = data.tiltY;
		if (data.layerSpacing != null && this.#layerSpacing !== data.layerSpacing) this.layerSpacing = data.layerSpacing;
		if (data.showOverlapHeight != null && this.#showOverlapHeight !== data.showOverlapHeight) this.showOverlapHeight = data.showOverlapHeight;
		if (data.mode !== undefined && this.#mode !== data.mode) this.mode = data.mode;
		if (data.space !== undefined && this.#space !== data.space) this.space = data.space;
	}
}
