import { getBoxToBoxArrow } from "perfect-arrows";
import { getStroke, type StrokeOptions } from "perfect-freehand";
import { FolkElement } from "./folk-element";
import { css } from "./tags";
import { isCompatible, dataTypeColor, inferFlowKind, flowKindColor } from "./data-types";

// Point interface for bezier curves
interface Point {
	x: number;
	y: number;
}

// Utility functions for bezier curve rendering
function lerp(a: Point, b: Point, t: number): Point {
	return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
}

function distanceSquared(a: Point, b: Point): number {
	const dx = b.x - a.x;
	const dy = b.y - a.y;
	return dx * dx + dy * dy;
}

function distance(a: Point, b: Point): number {
	return Math.sqrt(distanceSquared(a, b));
}

function flatness(points: readonly Point[], offset: number): number {
	const p1 = points[offset + 0];
	const p2 = points[offset + 1];
	const p3 = points[offset + 2];
	const p4 = points[offset + 3];

	let ux = 3 * p2.x - 2 * p1.x - p4.x;
	ux *= ux;
	let uy = 3 * p2.y - 2 * p1.y - p4.y;
	uy *= uy;
	let vx = 3 * p3.x - 2 * p4.x - p1.x;
	vx *= vx;
	let vy = 3 * p3.y - 2 * p4.y - p1.y;
	vy *= vy;

	if (ux < vx) ux = vx;
	if (uy < vy) uy = vy;

	return ux + uy;
}

function getPointsOnBezierCurveWithSplitting(
	points: readonly Point[],
	offset: number,
	tolerance: number,
	outPoints: Point[] = [],
): Point[] {
	if (flatness(points, offset) < tolerance) {
		const p0 = points[offset + 0];
		if (outPoints.length) {
			const d = distance(outPoints[outPoints.length - 1], p0);
			if (d > 1) {
				outPoints.push(p0);
			}
		} else {
			outPoints.push(p0);
		}
		outPoints.push(points[offset + 3]);
	} else {
		const t = 0.5;
		const p1 = points[offset + 0];
		const p2 = points[offset + 1];
		const p3 = points[offset + 2];
		const p4 = points[offset + 3];

		const q1 = lerp(p1, p2, t);
		const q2 = lerp(p2, p3, t);
		const q3 = lerp(p3, p4, t);

		const r1 = lerp(q1, q2, t);
		const r2 = lerp(q2, q3, t);

		const red = lerp(r1, r2, t);

		getPointsOnBezierCurveWithSplitting([p1, q1, r1, red], 0, tolerance, outPoints);
		getPointsOnBezierCurveWithSplitting([red, r2, q3, p4], 0, tolerance, outPoints);
	}
	return outPoints;
}

function pointsOnBezierCurves(points: readonly Point[], tolerance: number = 0.15): Point[] {
	const newPoints: Point[] = [];
	const numSegments = (points.length - 1) / 3;
	for (let i = 0; i < numSegments; i++) {
		const offset = i * 3;
		getPointsOnBezierCurveWithSplitting(points, offset, tolerance, newPoints);
	}
	return newPoints;
}

function getSvgPathFromStroke(stroke: number[][]): string {
	if (stroke.length === 0) return "";

	for (const point of stroke) {
		point[0] = Math.round(point[0] * 100) / 100;
		point[1] = Math.round(point[1] * 100) / 100;
	}

	const d = stroke.reduce(
		(acc, [x0, y0], i, arr) => {
			const [x1, y1] = arr[(i + 1) % arr.length];
			acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2);
			return acc;
		},
		["M", ...stroke[0], "Q"] as (string | number)[],
	);

	d.push("Z");
	return d.join(" ");
}

const styles = css`
	:host {
		display: block;
		position: absolute;
		inset: 0;
		pointer-events: none;
		z-index: -1;
	}
`;

declare global {
	interface HTMLElementTagNameMap {
		"folk-arrow": FolkArrow;
	}
}

export type ArrowStyle = "smooth" | "straight" | "curved" | "sketchy";

/** A governance gate on an arrow — data only flows when the gate is open. */
export interface ArrowGate {
	shapeId: string;       // governance shape ID
	portName: string;      // port to watch (e.g. "decision-out")
	condition: "truthy" | "passed" | "threshold" | "satisfied";
	threshold?: number;
}

/** An inline data transform on a pipe arrow. */
export interface ArrowTransform {
	type: "filter" | "map" | "pick" | "count" | "first" | "last";
	expression: string;  // safe dot-path, NO eval()
}

export class FolkArrow extends FolkElement {
	static override tagName = "folk-arrow";
	static styles = styles;

	#sourceSelector: string = "";
	#targetSelector: string = "";
	#sourceElement: Element | null = null;
	#targetElement: Element | null = null;
	#sourceRect: DOMRect | null = null;
	#targetRect: DOMRect | null = null;
	#resizeObserver: ResizeObserver;
	#color: string = "#374151";
	#strokeWidth: number = 3;
	#arrowStyle: ArrowStyle = "smooth";

	// Data piping
	#sourcePort: string = "";
	#targetPort: string = "";
	#pipeDebounce: ReturnType<typeof setTimeout> | null = null;
	#portListener: ((e: Event) => void) | null = null;

	// Flow bridge
	#flowId: string = "";

	get flowId() { return this.#flowId; }
	set flowId(value: string) { this.#flowId = value; }

	// Governance gate
	#gate: ArrowGate | null = null;
	#gateOpen: boolean = true;
	#gateListener: ((e: Event) => void) | null = null;

	get gate(): ArrowGate | null { return this.#gate; }
	set gate(value: ArrowGate | null) {
		this.#teardownGate();
		this.#gate = value;
		if (value) this.#setupGate();
		this.#updateArrow();
	}

	// Data transforms
	#transform: ArrowTransform | null = null;

	get transform(): ArrowTransform | null { return this.#transform; }
	set transform(value: ArrowTransform | null) {
		this.#transform = value;
		this.#updateArrow();
	}

	get sourcePort() { return this.#sourcePort; }
	set sourcePort(value: string) {
		this.#sourcePort = value;
		this.#setupPipe();
	}

	get targetPort() { return this.#targetPort; }
	set targetPort(value: string) {
		this.#targetPort = value;
		this.#setupPipe();
	}

	/** True when this arrow connects ports (is a data pipe). */
	get isPipe(): boolean {
		return !!(this.#sourcePort && this.#targetPort);
	}

	#options: StrokeOptions = {
		size: 7,
		thinning: 0.5,
		smoothing: 0.5,
		streamline: 0.5,
		simulatePressure: true,
		easing: (t) => t,
		start: {
			taper: 50,
			easing: (t) => t,
			cap: true,
		},
		end: {
			taper: 0,
			easing: (t) => t,
			cap: true,
		},
	};

	constructor() {
		super();
		this.#resizeObserver = new ResizeObserver(() => this.#updateArrow());
	}

	get source() {
		return this.#sourceSelector;
	}

	set source(value: string) {
		this.#sourceSelector = value;
		this.#observeSource();
		this.requestUpdate("source");
	}

	get target() {
		return this.#targetSelector;
	}

	set target(value: string) {
		this.#targetSelector = value;
		this.#observeTarget();
		this.requestUpdate("target");
	}

	get sourceId() {
		return this.#sourceSelector.replace("#", "");
	}

	set sourceId(value: string) {
		this.source = `#${value}`;
	}

	get targetId() {
		return this.#targetSelector.replace("#", "");
	}

	set targetId(value: string) {
		this.target = `#${value}`;
	}

	get color() {
		return this.#color;
	}

	set color(value: string) {
		this.#color = value;
		this.#updateArrow();
	}

	get strokeWidth() {
		return this.#strokeWidth;
	}

	set strokeWidth(value: number) {
		this.#strokeWidth = value;
		this.#options.size = value * 2 + 1;
		this.#updateArrow();
	}

	get arrowStyle(): ArrowStyle {
		return this.#arrowStyle;
	}

	set arrowStyle(value: ArrowStyle) {
		if (value === this.#arrowStyle) return;
		this.#arrowStyle = value;
		this.setAttribute("arrow-style", value);
		this.#updateArrow();
	}

	override connectedCallback() {
		super.connectedCallback();

		// Parse attributes
		const sourceAttr = this.getAttribute("source");
		const targetAttr = this.getAttribute("target");
		const colorAttr = this.getAttribute("color");
		const strokeAttr = this.getAttribute("stroke-width");
		const styleAttr = this.getAttribute("arrow-style");

		if (sourceAttr) this.source = sourceAttr;
		if (targetAttr) this.target = targetAttr;
		if (colorAttr) this.#color = colorAttr;
		if (strokeAttr) this.strokeWidth = parseFloat(strokeAttr);
		if (styleAttr) this.#arrowStyle = styleAttr as ArrowStyle;

		// Start animation frame loop for position updates
		this.#startPositionTracking();
	}

	override disconnectedCallback() {
		super.disconnectedCallback();
		this.#resizeObserver.disconnect();
		this.#stopPositionTracking();
		this.#teardownPipe();
		this.#teardownGate();
	}

	#animationFrameId: number | null = null;

	#startPositionTracking() {
		const track = () => {
			this.#updateRects();
			this.#updateArrow();
			this.#animationFrameId = requestAnimationFrame(track);
		};
		this.#animationFrameId = requestAnimationFrame(track);
	}

	#stopPositionTracking() {
		if (this.#animationFrameId !== null) {
			cancelAnimationFrame(this.#animationFrameId);
			this.#animationFrameId = null;
		}
	}

	/** Set up data pipe listener when both ports are connected. */
	#setupPipe(): void {
		this.#teardownPipe();

		if (!this.#sourcePort || !this.#targetPort) {
			// Reset arrow color when pipe is disconnected
			this.#updateArrow();
			return;
		}

		const srcEl = this.#sourceElement as any;
		const tgtEl = this.#targetElement as any;
		if (!srcEl || !tgtEl) return;

		// Check type compatibility
		const srcPort = srcEl.getPort?.(this.#sourcePort);
		const tgtPort = tgtEl.getPort?.(this.#targetPort);
		if (srcPort && tgtPort && !isCompatible(srcPort.type, tgtPort.type)) {
			console.warn(`[FolkArrow] Type mismatch: ${srcPort.type} -> ${tgtPort.type}`);
			return;
		}

		// Tint arrow color: use FlowKind if both endpoints are folk-rapp, else DataType
		if (srcPort) {
			const bothRapp = srcEl.tagName === "FOLK-RAPP" && tgtEl.tagName === "FOLK-RAPP";
			this.#color = bothRapp
				? flowKindColor(inferFlowKind(this.#sourcePort))
				: dataTypeColor(srcPort.type);
			this.#updateArrow();
		}

		// Listen for port value changes on the source
		this.#portListener = (e: Event) => {
			const detail = (e as CustomEvent).detail;
			if (detail.name !== this.#sourcePort) return;

			// Debounce to avoid rapid-fire updates
			if (this.#pipeDebounce) clearTimeout(this.#pipeDebounce);
			this.#pipeDebounce = setTimeout(() => {
				// Gate check: skip if gate is closed
				if (!this.#gateOpen) return;

				// Apply transform if present
				let value = this.#transform
					? this.#applyTransform(detail.value)
					: detail.value;

				if (tgtEl.setPortValue) {
					tgtEl.setPortValue(this.#targetPort, value);
				}
			}, 100);
		};

		srcEl.addEventListener("port-value-changed", this.#portListener);
	}

	/** Remove pipe listener. */
	#teardownPipe(): void {
		if (this.#portListener && this.#sourceElement) {
			this.#sourceElement.removeEventListener("port-value-changed", this.#portListener);
			this.#portListener = null;
		}
		if (this.#pipeDebounce) {
			clearTimeout(this.#pipeDebounce);
			this.#pipeDebounce = null;
		}
	}

	/** Set up gate listener on a governance shape. */
	#setupGate(): void {
		if (!this.#gate) return;
		const gateEl = document.getElementById(this.#gate.shapeId) as any;
		if (!gateEl) return;

		this.#gateListener = (e: Event) => {
			const detail = (e as CustomEvent).detail;
			if (detail.name !== this.#gate!.portName) return;
			this.#evaluateGate(detail.value);
		};

		gateEl.addEventListener("port-value-changed", this.#gateListener);

		// Evaluate current value immediately
		const currentVal = gateEl.getPortValue?.(this.#gate.portName);
		if (currentVal !== undefined) this.#evaluateGate(currentVal);
	}

	/** Remove gate listener. */
	#teardownGate(): void {
		if (this.#gateListener && this.#gate) {
			const gateEl = document.getElementById(this.#gate.shapeId);
			if (gateEl) {
				gateEl.removeEventListener("port-value-changed", this.#gateListener);
			}
		}
		this.#gateListener = null;
		this.#gateOpen = true;
	}

	/** Evaluate the gate condition against the current value. */
	#evaluateGate(value: unknown): void {
		const wasOpen = this.#gateOpen;

		if (!this.#gate) {
			this.#gateOpen = true;
		} else if (this.#gate.condition === "truthy") {
			this.#gateOpen = !!value;
		} else if (this.#gate.condition === "passed") {
			const v = value as any;
			this.#gateOpen = !!(v?.passed || v?.decided || v?.winner);
		} else if (this.#gate.condition === "threshold") {
			const v = value as any;
			const num = typeof v === "number" ? v : (v?.margin ?? v?.score ?? 0);
			this.#gateOpen = num >= (this.#gate.threshold ?? 0.5);
		} else if (this.#gate.condition === "satisfied") {
			this.#gateOpen = (value as any)?.satisfied === true;
		}

		if (wasOpen !== this.#gateOpen) this.#updateArrow();
	}

	/** Safe dot-path accessor: getPath({a: {b: 1}}, "a.b") → 1 */
	static #getPath(obj: any, path: string): any {
		if (!obj || !path) return obj;
		const parts = path.split(".");
		let current = obj;
		for (const part of parts) {
			if (current == null) return undefined;
			// Support bracket notation: items[0]
			const match = part.match(/^(\w+)\[(\d+)\]$/);
			if (match) {
				current = current[match[1]];
				if (Array.isArray(current)) current = current[parseInt(match[2])];
				else return undefined;
			} else {
				current = current[part];
			}
		}
		return current;
	}

	/** Evaluate a simple predicate (no eval): "status === 'DONE'" */
	static #evalPredicate(item: any, expr: string): boolean {
		// Parse: field op value
		const match = expr.match(/^([\w.[\]]+)\s*(===|!==|>=|<=|>|<)\s*['"]?([^'"]*?)['"]?$/);
		if (!match) return !!item;
		const [, field, op, val] = match;
		const fieldVal = FolkArrow.#getPath(item, field);
		const numVal = Number(val);
		const useNum = !isNaN(numVal) && typeof fieldVal === "number";
		switch (op) {
			case "===": return useNum ? fieldVal === numVal : String(fieldVal) === val;
			case "!==": return useNum ? fieldVal !== numVal : String(fieldVal) !== val;
			case ">":   return useNum ? fieldVal > numVal : false;
			case "<":   return useNum ? fieldVal < numVal : false;
			case ">=":  return useNum ? fieldVal >= numVal : false;
			case "<=":  return useNum ? fieldVal <= numVal : false;
			default: return false;
		}
	}

	/** Apply the transform to a data value. */
	#applyTransform(value: unknown): unknown {
		if (!this.#transform) return value;
		const { type, expression } = this.#transform;
		const arr = Array.isArray(value) ? value : [value];

		switch (type) {
			case "filter":
				return arr.filter(item => FolkArrow.#evalPredicate(item, expression));
			case "map":
				return arr.map(item => FolkArrow.#getPath(item, expression));
			case "pick":
				// Pick specific fields: "title,status,id"
				const fields = expression.split(",").map(f => f.trim());
				return arr.map(item => {
					const result: Record<string, any> = {};
					for (const f of fields) {
						result[f] = FolkArrow.#getPath(item, f);
					}
					return result;
				});
			case "count":
				return arr.length;
			case "first":
				return arr[0];
			case "last":
				return arr[arr.length - 1];
			default:
				return value;
		}
	}

	#observeSource() {
		if (this.#sourceElement) {
			this.#resizeObserver.unobserve(this.#sourceElement);
		}

		if (this.#sourceSelector) {
			this.#sourceElement = document.querySelector(this.#sourceSelector);
			if (this.#sourceElement) {
				this.#resizeObserver.observe(this.#sourceElement);
			}
		}
		if (this.isPipe) this.#setupPipe();
	}

	#observeTarget() {
		if (this.#targetElement) {
			this.#resizeObserver.unobserve(this.#targetElement);
		}

		if (this.#targetSelector) {
			this.#targetElement = document.querySelector(this.#targetSelector);
			if (this.#targetElement) {
				this.#resizeObserver.observe(this.#targetElement);
			}
		}
		if (this.isPipe) this.#setupPipe();
	}

	#updateRects() {
		if (this.#sourceElement) {
			this.#sourceRect = this.#sourceElement.getBoundingClientRect();
		}
		if (this.#targetElement) {
			this.#targetRect = this.#targetElement.getBoundingClientRect();
		}
	}

	#svg: SVGSVGElement | null = null;

	#updateArrow() {
		if (!this.#sourceRect || !this.#targetRect) {
			this.style.clipPath = "";
			this.style.display = "none";
			if (this.#svg) this.#svg.innerHTML = "";
			return;
		}

		this.style.display = "";

		const [sx, sy, cx, cy, ex, ey] = getBoxToBoxArrow(
			this.#sourceRect.x,
			this.#sourceRect.y,
			this.#sourceRect.width,
			this.#sourceRect.height,
			this.#targetRect.x,
			this.#targetRect.y,
			this.#targetRect.width,
			this.#targetRect.height,
		);

		// Gate visual: dim and use gray when gated closed
		const effectiveColor = (this.#gate && !this.#gateOpen) ? "#94a3b8" : this.#color;
		const gatedClosed = this.#gate && !this.#gateOpen;

		if (this.#arrowStyle === "smooth") {
			// Original behavior: perfect-freehand tapered stroke via clipPath
			if (this.#svg) this.#svg.style.display = "none";
			this.style.clipPath = "";
			this.style.backgroundColor = "";

			const points = pointsOnBezierCurves([
				{ x: sx, y: sy },
				{ x: cx, y: cy },
				{ x: ex, y: ey },
				{ x: ex, y: ey },
			]);

			const stroke = getStroke(points, this.#options);
			const path = getSvgPathFromStroke(stroke);

			this.style.clipPath = `path('${path}')`;
			this.style.backgroundColor = effectiveColor;
			this.style.opacity = gatedClosed ? "0.4" : "";
		} else {
			// SVG-based rendering for other styles
			this.style.clipPath = "";
			this.style.backgroundColor = "";

			if (!this.#svg) return;
			this.#svg.style.display = "";

			const sw = this.#strokeWidth;
			let pathD = "";
			let arrowheadPoints = "";

			if (this.#arrowStyle === "straight") {
				pathD = `M ${sx} ${sy} L ${ex} ${ey}`;
			} else if (this.#arrowStyle === "curved") {
				pathD = `M ${sx} ${sy} Q ${cx} ${cy} ${ex} ${ey}`;
			} else if (this.#arrowStyle === "sketchy") {
				// Jitter the control point for hand-drawn feel
				const jitter = () => (Math.random() - 0.5) * 12;
				const jcx = cx + jitter();
				const jcy = cy + jitter();
				pathD = `M ${sx + jitter() * 0.3} ${sy + jitter() * 0.3} Q ${jcx} ${jcy} ${ex} ${ey}`;
			}

			// Compute arrowhead at the end point
			// Get the tangent direction at the end of the path
			let dx: number, dy: number;
			if (this.#arrowStyle === "straight") {
				dx = ex - sx;
				dy = ey - sy;
			} else {
				// For curves, tangent at end = end - control
				dx = ex - cx;
				dy = ey - cy;
			}
			const len = Math.sqrt(dx * dx + dy * dy) || 1;
			const ux = dx / len;
			const uy = dy / len;
			const headLen = sw * 4;
			const headW = sw * 2.5;
			// Perpendicular
			const px = -uy;
			const py = ux;
			const p1x = ex - ux * headLen + px * headW;
			const p1y = ey - uy * headLen + py * headW;
			const p2x = ex - ux * headLen - px * headW;
			const p2y = ey - uy * headLen - py * headW;

			if (this.#arrowStyle === "sketchy") {
				const j = () => (Math.random() - 0.5) * 3;
				arrowheadPoints = `${p1x + j()},${p1y + j()} ${ex + j()},${ey + j()} ${p2x + j()},${p2y + j()}`;
			} else {
				arrowheadPoints = `${p1x},${p1y} ${ex},${ey} ${p2x},${p2y}`;
			}

			const dashAttr = gatedClosed
				? 'stroke-dasharray="6 4"'
				: (this.#arrowStyle === "sketchy" ? 'stroke-dasharray="0"' : "");
			const opacityAttr = gatedClosed ? ' opacity="0.4"' : "";

			let svgContent = `
				<path d="${pathD}" fill="none" stroke="${effectiveColor}" stroke-width="${sw}"
					stroke-linecap="round" stroke-linejoin="round"
					${dashAttr}${opacityAttr} />
				<polygon points="${arrowheadPoints}" fill="${effectiveColor}"${opacityAttr} />
			`;

			// Transform badge at midpoint
			if (this.#transform) {
				const badges: Record<string, string> = { filter: "F", map: "M", pick: "P", count: "#", first: "1", last: "L" };
				const badge = badges[this.#transform.type] || "T";
				svgContent += `
					<circle cx="${cx}" cy="${cy}" r="10" fill="white" stroke="${effectiveColor}" stroke-width="1.5" />
					<text x="${cx}" y="${cy}" text-anchor="middle" dominant-baseline="central"
						fill="${effectiveColor}" font-size="10" font-weight="700">${badge}</text>
				`;
			}

			this.#svg.innerHTML = svgContent;
		}
	}

	override createRenderRoot() {
		const root = super.createRenderRoot();
		this.#svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
		this.#svg.style.cssText = "position:absolute;inset:0;width:100%;height:100%;overflow:visible;pointer-events:none;";
		(root as ShadowRoot).appendChild(this.#svg);
		this.#updateArrow();
		return root;
	}

	static fromData(data: Record<string, any>): FolkArrow {
		const arrow = document.createElement("folk-arrow") as FolkArrow;
		arrow.id = data.id;
		if (data.sourceId) arrow.sourceId = data.sourceId;
		if (data.targetId) arrow.targetId = data.targetId;
		if (data.color) arrow.color = data.color;
		if (data.strokeWidth) arrow.strokeWidth = data.strokeWidth;
		if (data.arrowStyle) arrow.arrowStyle = data.arrowStyle;
		if (data.sourcePort) arrow.sourcePort = data.sourcePort;
		if (data.targetPort) arrow.targetPort = data.targetPort;
		if (data.flowId) arrow.flowId = data.flowId;
		if (data.gate) arrow.gate = data.gate;
		if (data.transform) arrow.transform = data.transform;
		return arrow;
	}

	toJSON() {
		const json: Record<string, unknown> = {
			type: "folk-arrow",
			id: this.id,
			sourceId: this.sourceId,
			targetId: this.targetId,
			color: this.#color,
			strokeWidth: this.#strokeWidth,
			arrowStyle: this.#arrowStyle,
		};
		if (this.#sourcePort) json.sourcePort = this.#sourcePort;
		if (this.#targetPort) json.targetPort = this.#targetPort;
		if (this.#flowId) json.flowId = this.#flowId;
		if (this.#gate) json.gate = this.#gate;
		if (this.#transform) json.transform = this.#transform;
		return json;
	}

	applyData(data: Record<string, any>): void {
		if (data.sourceId !== undefined && this.sourceId !== data.sourceId) this.sourceId = data.sourceId;
		if (data.targetId !== undefined && this.targetId !== data.targetId) this.targetId = data.targetId;
		if (data.color !== undefined && this.#color !== data.color) this.color = data.color;
		if (data.strokeWidth !== undefined && this.#strokeWidth !== data.strokeWidth) this.strokeWidth = data.strokeWidth;
		if (data.arrowStyle !== undefined && this.#arrowStyle !== data.arrowStyle) this.arrowStyle = data.arrowStyle as ArrowStyle;
		if (data.sourcePort !== undefined && this.#sourcePort !== data.sourcePort) this.sourcePort = data.sourcePort;
		if (data.targetPort !== undefined && this.#targetPort !== data.targetPort) this.targetPort = data.targetPort;
		if (data.flowId !== undefined && this.#flowId !== data.flowId) this.flowId = data.flowId;
		if (data.gate !== undefined) this.gate = data.gate;
		if (data.transform !== undefined) this.transform = data.transform;
	}
}
