/**
 * folk-commitment-pool — Canvas shape rendering a physics-based basket of
 * commitment orbs. Orbs can be dragged out onto folk-task-request shapes.
 *
 * Extracted from modules/rtime/components/folk-timebank-app.ts (pool view).
 */

import { FolkShape } from "./folk-shape";
import { css, html } from "./tags";
import { getModuleApiBase } from "../shared/url-helpers";
import type { PortDescriptor } from "./data-types";

// ── Skill constants (mirrored from rtime/schemas to avoid server import) ──

const SKILL_COLORS: Record<string, string> = {
	facilitation: '#8b5cf6', design: '#ec4899', tech: '#3b82f6',
	outreach: '#10b981', logistics: '#f59e0b',
};
const SKILL_LABELS: Record<string, string> = {
	facilitation: 'Facilitation', design: 'Design', tech: 'Tech',
	outreach: 'Outreach', logistics: 'Logistics',
};

// ── Types ──

interface PoolCommitment {
	id: string;
	memberName: string;
	hours: number;
	skill: string;
	desc: string;
	ownerDid?: string;
}

// ── Orb class ──

class Orb {
	c: PoolCommitment;
	baseRadius: number;
	radius: number;
	x: number;
	y: number;
	vx: number;
	vy: number;
	hoverT = 0;
	phase: number;
	opacity = 0;
	color: string;

	constructor(c: PoolCommitment, cx: number, cy: number, r: number) {
		this.c = c;
		// Scale orb radius relative to basket size so they always fit
		// Base: 8-15% of basket radius, scaled by sqrt(hours) to avoid giant orbs
		const minR = r * 0.08;
		const maxR = r * 0.15;
		this.baseRadius = Math.min(maxR, minR + (maxR - minR) * (Math.sqrt(c.hours) / Math.sqrt(10)));
		this.radius = this.baseRadius;
		const a = Math.random() * Math.PI * 2;
		const d = Math.random() * (r - this.baseRadius - 10);
		this.x = cx + Math.cos(a) * d;
		this.y = cy + Math.sin(a) * d;
		this.vx = (Math.random() - 0.5) * 0.4;
		this.vy = (Math.random() - 0.5) * 0.4;
		this.phase = Math.random() * Math.PI * 2;
		this.color = SKILL_COLORS[c.skill] || '#8b5cf6';
	}

	update(hovered: Orb | null, cx: number, cy: number, r: number) {
		this.phase += 0.008;
		this.vx += Math.sin(this.phase) * 0.003;
		this.vy += Math.cos(this.phase * 0.73 + 1) * 0.003;
		this.vx *= 0.996;
		this.vy *= 0.996;
		this.x += this.vx;
		this.y += this.vy;

		const dx = this.x - cx, dy = this.y - cy;
		const dist = Math.sqrt(dx * dx + dy * dy);
		const maxDist = r - this.radius - 3;
		if (dist > maxDist && dist > 0.1) {
			const nx = dx / dist, ny = dy / dist;
			this.x = cx + nx * maxDist;
			this.y = cy + ny * maxDist;
			const dot = this.vx * nx + this.vy * ny;
			this.vx -= 1.3 * dot * nx;
			this.vy -= 1.3 * dot * ny;
			this.vx *= 0.6; this.vy *= 0.6;
		}

		const isH = hovered === this;
		this.hoverT += ((isH ? 1 : 0) - this.hoverT) * 0.12;
		this.radius = this.baseRadius * (1 + this.hoverT * 0.15);
		if (this.opacity < 1) this.opacity = Math.min(1, this.opacity + 0.025);
	}

	draw(ctx: CanvasRenderingContext2D) {
		if (this.opacity < 0.01) return;
		ctx.save();
		ctx.globalAlpha = this.opacity;
		if (this.hoverT > 0.05) { ctx.shadowColor = this.color; ctx.shadowBlur = 24 * this.hoverT; }

		ctx.beginPath();
		ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
		ctx.fillStyle = this.color + '15';
		ctx.fill();

		ctx.beginPath();
		ctx.arc(this.x, this.y, this.radius * 0.82, 0, Math.PI * 2);
		const g = ctx.createRadialGradient(
			this.x - this.radius * 0.15, this.y - this.radius * 0.15, 0,
			this.x, this.y, this.radius * 0.82
		);
		g.addColorStop(0, this.color + 'dd');
		g.addColorStop(1, this.color);
		ctx.fillStyle = g;
		ctx.fill();
		ctx.shadowBlur = 0;
		ctx.strokeStyle = this.color;
		ctx.lineWidth = 1.5;
		ctx.stroke();

		if (this.hoverT > 0.05) {
			ctx.globalAlpha = this.opacity * 0.08 * this.hoverT;
			ctx.beginPath();
			ctx.ellipse(this.x, this.y + this.radius + 4, this.radius * 0.6, 4, 0, 0, Math.PI * 2);
			ctx.fillStyle = '#000';
			ctx.fill();
			ctx.globalAlpha = this.opacity;
		}

		ctx.fillStyle = '#fff';
		ctx.textAlign = 'center';
		ctx.textBaseline = 'middle';
		const fs = Math.max(10, this.radius * 0.3);
		ctx.font = '600 ' + fs + 'px -apple-system, sans-serif';
		ctx.fillText(this.c.memberName.split(' ')[0], this.x, this.y - fs * 0.35);
		ctx.font = '500 ' + (fs * 0.78) + 'px -apple-system, sans-serif';
		ctx.fillStyle = '#ffffffcc';
		ctx.fillText(this.c.hours + 'hr \u00b7 ' + (SKILL_LABELS[this.c.skill] || this.c.skill), this.x, this.y + fs * 0.55);

		ctx.restore();
	}

	contains(px: number, py: number) {
		const dx = px - this.x, dy = py - this.y;
		return dx * dx + dy * dy < this.radius * this.radius;
	}
}

// ── Ripple class ──

class Ripple {
	x: number; y: number; r = 8; o = 0.5; color: string;
	constructor(x: number, y: number, color: string) { this.x = x; this.y = y; this.color = color; }
	update() { this.r += 1.5; this.o -= 0.008; return this.o > 0; }
	draw(ctx: CanvasRenderingContext2D) {
		ctx.save(); ctx.globalAlpha = this.o;
		ctx.strokeStyle = this.color; ctx.lineWidth = 2;
		ctx.beginPath(); ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2); ctx.stroke();
		ctx.restore();
	}
}

// ── Styles ──

const styles = css`
	:host {
		background: transparent;
		border-radius: 12px;
		overflow: hidden;
	}
	canvas {
		width: 100%;
		height: 100%;
		display: block;
	}
	.empty-msg {
		position: absolute;
		inset: 0;
		display: flex;
		align-items: center;
		justify-content: center;
		color: #94a3b8;
		font-size: 13px;
		font-style: italic;
		pointer-events: none;
	}
`;

declare global {
	interface HTMLElementTagNameMap {
		"folk-commitment-pool": FolkCommitmentPool;
	}
}

export class FolkCommitmentPool extends FolkShape {
	static override tagName = "folk-commitment-pool";

	static override portDescriptors: PortDescriptor[] = [
		{ name: "pool-out", type: "json", direction: "output" },
	];

	static {
		const sheet = new CSSStyleSheet();
		const parentRules = Array.from(FolkShape.styles.cssRules).map(r => r.cssText).join("\n");
		const childRules = Array.from(styles.cssRules).map(r => r.cssText).join("\n");
		sheet.replaceSync(`${parentRules}\n${childRules}`);
		this.styles = sheet;
	}

	// ── Module-disabled gating ──
	static #enabledModules: Set<string> | null = null;
	static #instances = new Set<FolkCommitmentPool>();

	static setEnabledModules(ids: string[] | null) {
		FolkCommitmentPool.#enabledModules = ids ? new Set(ids) : null;
		for (const inst of FolkCommitmentPool.#instances) inst.#syncDisabledState();
	}

	#isModuleDisabled(): boolean {
		const enabled = FolkCommitmentPool.#enabledModules;
		if (!enabled) return false;
		return !enabled.has("rtime");
	}

	#syncDisabledState() {
		if (!this.#wrapper) return;
		const disabled = this.#isModuleDisabled();
		const wasDisabled = this.hasAttribute("data-module-disabled");
		if (disabled && !wasDisabled) {
			this.#showDisabledOverlay();
		} else if (!disabled && wasDisabled) {
			this.removeAttribute("data-module-disabled");
			this.#wrapper.querySelector(".disabled-overlay")?.remove();
			this.#fetchCommitments();
			this.#startAnimation();
		}
	}

	#showDisabledOverlay() {
		this.setAttribute("data-module-disabled", "");
		if (this.#animFrame) { cancelAnimationFrame(this.#animFrame); this.#animFrame = 0; }
		let overlay = this.#wrapper?.querySelector(".disabled-overlay") as HTMLElement;
		if (!overlay && this.#wrapper) {
			overlay = document.createElement("div");
			overlay.className = "disabled-overlay";
			overlay.style.cssText = "position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:8px;background:rgba(15,23,42,0.85);border-radius:inherit;z-index:10;color:#94a3b8;font-size:13px;";
			overlay.innerHTML = '<span style="font-size:24px">🔒</span><span>rTime is disabled</span>';
			this.#wrapper.appendChild(overlay);
		}
	}

	#spaceSlug = "demo";
	#canvas!: HTMLCanvasElement;
	#ctx!: CanvasRenderingContext2D;
	#orbs: Orb[] = [];
	#ripples: Ripple[] = [];
	#animFrame = 0;
	#hoveredOrb: Orb | null = null;
	#draggingOrb: Orb | null = null;
	#ghost: HTMLDivElement | null = null;
	#dpr = 1;
	#wrapper!: HTMLElement;

	get spaceSlug() { return this.#spaceSlug; }
	set spaceSlug(v: string) {
		this.#spaceSlug = v;
		this.#fetchCommitments();
	}

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

		this.#wrapper = document.createElement("div");
		this.#wrapper.style.cssText = "width:100%;height:100%;position:relative;";
		this.#wrapper.innerHTML = html`<canvas></canvas><div class="empty-msg" style="display:none">No commitments yet</div>`;

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

		this.#canvas = this.#wrapper.querySelector("canvas")!;
		this.#canvas.style.touchAction = "none"; // prevent browser scroll/pan on touch drag
		this.#ctx = this.#canvas.getContext("2d")!;

		this.#canvas.addEventListener("pointermove", this.#onPointerMove);
		this.#canvas.addEventListener("pointerdown", this.#onPointerDown);
		this.#canvas.addEventListener("pointerleave", () => { this.#hoveredOrb = null; });

		FolkCommitmentPool.#instances.add(this);

		if (this.#isModuleDisabled()) {
			this.#showDisabledOverlay();
		} else {
			this.#fetchCommitments();
			this.#startAnimation();
		}

		return root;
	}

	disconnectedCallback() {
		FolkCommitmentPool.#instances.delete(this);
		if (this.#animFrame) cancelAnimationFrame(this.#animFrame);
		this.#animFrame = 0;
		this.#removeGhost();
		document.removeEventListener("pointermove", this.#onDocPointerMove);
		document.removeEventListener("pointerup", this.#onDocPointerUp);
	}

	// ── Data fetching ──

	async #fetchCommitments() {
		try {
			const resp = await fetch(`${getModuleApiBase("rtime")}/api/commitments`);
			if (!resp.ok) return;
			const data = await resp.json();
			const commitments: PoolCommitment[] = data.commitments || [];
			this.#rebuildOrbs(commitments);
		} catch { /* network error, keep existing orbs */ }
	}

	#rebuildOrbs(commitments: PoolCommitment[]) {
		const w = this.width || 500;
		const h = this.height || 500;
		const cx = w / 2, cy = h / 2;
		const r = Math.min(cx, cy) * 0.85;

		const emptyMsg = this.#wrapper.querySelector(".empty-msg") as HTMLElement;
		if (emptyMsg) emptyMsg.style.display = commitments.length === 0 ? "flex" : "none";

		// Preserve existing orbs by commitment ID, rescale to current basket size
		const existing = new Map(this.#orbs.map(o => [o.c.id, o]));
		this.#orbs = commitments.map(c => {
			const old = existing.get(c.id);
			if (old) {
				// Rescale existing orb to current basket radius
				const minR = r * 0.08;
				const maxR = r * 0.15;
				old.baseRadius = Math.min(maxR, minR + (maxR - minR) * (Math.sqrt(c.hours) / Math.sqrt(10)));
				return old;
			}
			return new Orb(c, cx, cy, r);
		});

		// Emit pool snapshot on output port
		this.#emitPoolSnapshot(commitments);
	}

	#emitPoolSnapshot(commitments: PoolCommitment[]) {
		const bySkill: Record<string, { available: number; committed: number }> = {};
		let totalHours = 0;
		for (const c of commitments) {
			totalHours += c.hours;
			if (!bySkill[c.skill]) bySkill[c.skill] = { available: 0, committed: 0 };
			bySkill[c.skill].available += c.hours;
		}
		this.setPortValue("pool-out", { totalHours, bySkill, count: commitments.length });
	}

	// ── Canvas coord helpers ──

	#canvasCoords(e: PointerEvent): { x: number; y: number } {
		const rect = this.#canvas.getBoundingClientRect();
		return {
			x: (e.clientX - rect.left) * (this.#canvas.width / this.#dpr / rect.width),
			y: (e.clientY - rect.top) * (this.#canvas.height / this.#dpr / rect.height),
		};
	}

	#findOrbAt(x: number, y: number): Orb | null {
		for (let i = this.#orbs.length - 1; i >= 0; i--) {
			if (this.#orbs[i].contains(x, y)) return this.#orbs[i];
		}
		return null;
	}

	// ── Pointer handlers ──

	#onPointerMove = (e: PointerEvent) => {
		if (this.#draggingOrb) return; // drag is handled on document
		const { x, y } = this.#canvasCoords(e);
		const orb = this.#findOrbAt(x, y);
		this.#hoveredOrb = orb;
		this.#canvas.style.cursor = orb ? "grab" : "default";
	};

	#onPointerDown = (e: PointerEvent) => {
		const { x, y } = this.#canvasCoords(e);
		const orb = this.#findOrbAt(x, y);
		if (!orb) return;

		// Prevent FolkShape from starting a shape-move + browser scroll/pan on touch
		e.stopPropagation();
		e.preventDefault();

		this.#draggingOrb = orb;
		this.#ripples.push(new Ripple(orb.x, orb.y, orb.color));

		// Create ghost element on document.body
		this.#ghost = document.createElement("div");
		this.#ghost.style.cssText = `
			position: fixed; z-index: 99999; pointer-events: none;
			width: ${orb.radius * 2}px; height: ${orb.radius * 2}px;
			border-radius: 50%;
			background: ${orb.color};
			opacity: 0.8;
			box-shadow: 0 0 20px ${orb.color}80;
			display: flex; align-items: center; justify-content: center;
			color: white; font-size: 11px; font-weight: 600;
			font-family: -apple-system, sans-serif;
			transform: translate(-50%, -50%);
			left: ${e.clientX}px; top: ${e.clientY}px;
		`;
		this.#ghost.textContent = `${orb.c.hours}hr ${SKILL_LABELS[orb.c.skill] || orb.c.skill}`;
		document.body.appendChild(this.#ghost);

		// Dispatch drag start
		document.dispatchEvent(new CustomEvent("commitment-drag-start", {
			detail: {
				commitmentId: orb.c.id,
				skill: orb.c.skill,
				hours: orb.c.hours,
				memberName: orb.c.memberName,
				ownerDid: orb.c.ownerDid,
			},
		}));

		document.addEventListener("pointermove", this.#onDocPointerMove);
		document.addEventListener("pointerup", this.#onDocPointerUp);
	};

	#onDocPointerMove = (e: PointerEvent) => {
		if (this.#ghost) {
			this.#ghost.style.left = `${e.clientX}px`;
			this.#ghost.style.top = `${e.clientY}px`;
		}
		document.dispatchEvent(new CustomEvent("commitment-drag-move", {
			detail: { clientX: e.clientX, clientY: e.clientY },
		}));
	};

	#onDocPointerUp = (e: PointerEvent) => {
		document.removeEventListener("pointermove", this.#onDocPointerMove);
		document.removeEventListener("pointerup", this.#onDocPointerUp);

		document.dispatchEvent(new CustomEvent("commitment-drag-end", {
			detail: {
				clientX: e.clientX,
				clientY: e.clientY,
				commitmentId: this.#draggingOrb?.c.id,
				skill: this.#draggingOrb?.c.skill,
				hours: this.#draggingOrb?.c.hours,
				memberName: this.#draggingOrb?.c.memberName,
				ownerDid: this.#draggingOrb?.c.ownerDid,
			},
		}));

		this.#draggingOrb = null;
		this.#removeGhost();
	};

	#removeGhost() {
		if (this.#ghost) {
			this.#ghost.remove();
			this.#ghost = null;
		}
	}

	// ── Animation loop ──

	#startAnimation() {
		const frame = () => {
			this.#animFrame = requestAnimationFrame(frame);
			this.#poolFrame();
		};
		this.#animFrame = requestAnimationFrame(frame);
	}

	#poolFrame() {
		const w = this.width || 500;
		const h = this.height || 500;
		const cx = w / 2, cy = h / 2;
		const r = Math.min(cx, cy) * 0.85;

		// Resize canvas if needed
		this.#dpr = window.devicePixelRatio || 1;
		const cw = Math.round(w * this.#dpr);
		const ch = Math.round(h * this.#dpr);
		if (this.#canvas.width !== cw || this.#canvas.height !== ch) {
			this.#canvas.width = cw;
			this.#canvas.height = ch;
		}

		const ctx = this.#ctx;
		ctx.setTransform(this.#dpr, 0, 0, this.#dpr, 0, 0);
		ctx.clearRect(0, 0, w, h);

		// Draw basket
		ctx.save();
		ctx.strokeStyle = '#334155';
		ctx.lineWidth = 2;
		ctx.setLineDash([8, 4]);
		ctx.beginPath();
		ctx.arc(cx, cy, r, 0, Math.PI * 2);
		ctx.stroke();
		ctx.setLineDash([]);

		// Basket label
		ctx.fillStyle = '#475569';
		ctx.font = '500 12px -apple-system, sans-serif';
		ctx.textAlign = 'center';
		ctx.fillText('Commitment Pool', cx, cy - r + 20);
		ctx.fillStyle = '#64748b';
		ctx.font = '400 11px -apple-system, sans-serif';
		ctx.fillText(`${this.#orbs.length} pledges`, cx, cy - r + 36);
		ctx.restore();

		// Update & draw ripples
		this.#ripples = this.#ripples.filter(rp => { rp.update(); rp.draw(ctx); return rp.o > 0; });

		// Orb-to-orb collision
		for (let i = 0; i < this.#orbs.length; i++) {
			for (let j = i + 1; j < this.#orbs.length; j++) {
				const a = this.#orbs[i], b = this.#orbs[j];
				const dx = b.x - a.x, dy = b.y - a.y;
				const dist = Math.sqrt(dx * dx + dy * dy);
				const minD = a.radius + b.radius + 2;
				if (dist < minD && dist > 0.01) {
					const nx = dx / dist, ny = dy / dist;
					const overlap = (minD - dist) / 2;
					a.x -= nx * overlap; a.y -= ny * overlap;
					b.x += nx * overlap; b.y += ny * overlap;
					const relV = (a.vx - b.vx) * nx + (a.vy - b.vy) * ny;
					if (relV > 0) {
						a.vx -= relV * nx * 0.5; a.vy -= relV * ny * 0.5;
						b.vx += relV * nx * 0.5; b.vy += relV * ny * 0.5;
					}
				}
			}
		}

		// Update & draw orbs
		for (const orb of this.#orbs) {
			if (orb === this.#draggingOrb) {
				orb.opacity = 0.3;
				orb.draw(ctx);
				continue;
			}
			orb.update(this.#hoveredOrb, cx, cy, r);
			orb.draw(ctx);
		}
	}

	// ── Serialization ──

	override toJSON() {
		return {
			...super.toJSON(),
			type: "folk-commitment-pool",
			spaceSlug: this.#spaceSlug,
		};
	}

	static override fromData(data: Record<string, any>): FolkCommitmentPool {
		const shape = FolkShape.fromData(data) as FolkCommitmentPool;
		if (data.spaceSlug) shape.spaceSlug = data.spaceSlug;
		return shape;
	}

	override applyData(data: Record<string, any>): void {
		super.applyData(data);
		if (data.spaceSlug && data.spaceSlug !== this.#spaceSlug) this.spaceSlug = data.spaceSlug;
	}
}
