/**
 * <folk-crm-view> — API-driven CRM interface.
 *
 * Tabbed view: Pipeline | Contacts | Companies | Graph
 * Fetches from per-space API endpoints on the rNetwork module.
 */

interface Opportunity {
	id: string;
	name: string;
	stage: string;
	amount?: { amountMicros: number; currencyCode: string };
	company?: { id: string; name: string };
	pointOfContact?: { id: string; name: { firstName: string; lastName: string } };
	createdAt?: string;
	closeDate?: string;
}

interface Person {
	id: string;
	name: { firstName: string; lastName: string };
	email?: { primaryEmail: string };
	phone?: { primaryPhoneNumber: string };
	city?: string;
	company?: { id: string; name: { firstName: string; lastName: string } };
	createdAt?: string;
}

interface Company {
	id: string;
	name: string;
	domainName?: { primaryLinkUrl: string };
	employees?: number;
	address?: { addressCity: string; addressCountry: string };
	createdAt?: string;
}

interface CrmGraphNode {
	id: string;
	name: string;
	type: "person" | "company";
	companyId?: string;
}

interface CrmGraphEdge {
	source: string;
	target: string;
	type: "works_at" | "point_of_contact";
}

type Tab = "pipeline" | "contacts" | "companies" | "graph" | "delegations";

const PIPELINE_STAGES = [
	"INCOMING",
	"MEETING",
	"PROPOSAL",
	"CLOSED_WON",
	"CLOSED_LOST",
];

const STAGE_LABELS: Record<string, string> = {
	INCOMING: "Incoming",
	MEETING: "Meeting",
	PROPOSAL: "Proposal",
	CLOSED_WON: "Won",
	CLOSED_LOST: "Lost",
};

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

class FolkCrmView extends HTMLElement {
	private shadow: ShadowRoot;
	private space = "";
	private activeTab: Tab = "pipeline";
	private _history = new ViewHistory<Tab>("pipeline", "rnetwork");
	private searchQuery = "";
	private sortColumn = "";
	private sortAsc = true;

	private opportunities: Opportunity[] = [];
	private people: Person[] = [];
	private companies: Company[] = [];
	private loading = true;
	private error = "";

	// Graph state
	private graphNodes: CrmGraphNode[] = [];
	private graphEdges: CrmGraphEdge[] = [];
	private graphPositions: Record<string, { x: number; y: number }> = {};
	private graphLayoutDirty = true;
	private graphZoom = 1;
	private graphPanX = 0;
	private graphPanY = 0;
	private graphDraggingId: string | null = null;
	private graphDragStartX = 0;
	private graphDragStartY = 0;
	private graphDragNodeStartX = 0;
	private graphDragNodeStartY = 0;
	private graphIsPanning = false;
	private graphPanStartX = 0;
	private graphPanStartY = 0;
	private graphPanStartPanX = 0;
	private graphPanStartPanY = 0;
	private graphSelectedId: string | null = null;

	private _stopPresence: (() => void) | null = null;
	private _subscribedDocIds: string[] = [];

	// Guided tour
	private _tour!: TourEngine;
	private static readonly TOUR_STEPS = [
		{ target: '.crm-header', title: "CRM Overview", message: "Track deals through stages — from incoming leads to closed-won. Use the tab bar above to switch between Pipeline, Contacts, Companies, Graph, and Delegations.", advanceOnClick: false },
		{ target: '#crm-search', title: "Search", message: "Search across contacts and companies by name, email, or city. Results filter in real time.", advanceOnClick: false },
		{ target: '.crm-content', title: "Content Area", message: "Each tab shows a different view: Pipeline cards, contact/company tables, a relationship graph, or delegation management.", advanceOnClick: false },
	];

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

	private _onTabChange = (e: Event) => {
		const tab = (e as CustomEvent).detail?.tab;
		if (tab && tab !== this.activeTab) {
			this._history.push(tab as Tab);
			this.activeTab = tab as Tab;
			this.searchQuery = "";
			this.sortColumn = "";
			this.sortAsc = true;
			this.render();
		}
	};

	connectedCallback() {
		this.space = this.getAttribute("space") || "demo";
		// Read initial tab from URL pathname (e.g. /{space}/rnetwork/crm/contacts)
		const pathSegments = window.location.pathname.replace(/\/$/, '').split('/');
		const lastSegment = pathSegments[pathSegments.length - 1];
		if (lastSegment && ["pipeline", "contacts", "companies", "graph", "delegations"].includes(lastSegment)) {
			this.activeTab = lastSegment as Tab;
		} else {
			// Fallback: check ?tab= query param for backward compat
			const params = new URLSearchParams(window.location.search);
			const urlTab = params.get("tab");
			if (urlTab && ["pipeline", "contacts", "companies", "graph", "delegations"].includes(urlTab)) {
				this.activeTab = urlTab as Tab;
			}
		}
		// Listen for server-rendered tab bar changes
		document.addEventListener("rapp-tab-change", this._onTabChange);
		this.render();
		this.loadData();
		if (this.space !== "demo") {
			this.subscribeCollabOverlay();
		}
		// Auto-start tour on first visit
		if (!localStorage.getItem("rnetwork_tour_done")) {
			setTimeout(() => this._tour.start(), 1200);
		}
		this._stopPresence = startPresenceHeartbeat(() => ({ module: 'rnetwork', context: this.graphSelectedId ? 'CRM' : `CRM - ${this.activeTab}` }));
		window.addEventListener('rspace-view-restored', this._onViewRestored as EventListener);
	}

	disconnectedCallback() {
		this._history.destroy();
		window.removeEventListener('rspace-view-restored', this._onViewRestored as EventListener);
		document.removeEventListener("rapp-tab-change", this._onTabChange);
		this._stopPresence?.();
		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 !== 'rnetwork') return;
		this.activeTab = e.detail.view;
		this.searchQuery = "";
		this.render();
	};

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

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

	private getAuthUrl(): string {
		// Check attribute first, then meta tag, then location-based fallback
		const attr = this.getAttribute('auth-url');
		if (attr) return attr;
		const meta = document.querySelector('meta[name="encryptid-url"]');
		if (meta) return meta.getAttribute('content') || '';
		// Fallback: same origin for dev, auth subdomain for production
		if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') return '';
		return `https://auth.${location.hostname.split('.').slice(-2).join('.')}`;
	}

	private async loadData() {
		const base = this.getApiBase();
		try {
			const [oppRes, peopleRes, compRes] = await Promise.all([
				fetch(`${base}/api/opportunities`),
				fetch(`${base}/api/people`),
				fetch(`${base}/api/companies`),
			]);
			if (oppRes.ok) {
				const d = await oppRes.json();
				this.opportunities = d.opportunities || [];
			}
			if (peopleRes.ok) {
				const d = await peopleRes.json();
				this.people = d.people || [];
			}
			if (compRes.ok) {
				const d = await compRes.json();
				this.companies = d.companies || [];
			}
		} catch (e) {
			this.error = "Failed to load CRM data";
		}
		this.graphLayoutDirty = true;
		this.loading = false;
		this.render();
	}

	private formatAmount(amount?: { amountMicros: number; currencyCode: string }): string {
		if (!amount || !amount.amountMicros) return "-";
		const value = amount.amountMicros / 1_000_000;
		const currency = amount.currencyCode || "USD";
		try {
			return new Intl.NumberFormat("en-US", { style: "currency", currency, maximumFractionDigits: 0 }).format(value);
		} catch {
			return `${currency} ${value.toLocaleString()}`;
		}
	}

	private personName(p?: { firstName: string; lastName: string }): string {
		if (!p) return "-";
		return [p.firstName, p.lastName].filter(Boolean).join(" ") || "-";
	}

	private companyName(c?: { id: string; name: string | { firstName: string; lastName: string } }): string {
		if (!c) return "-";
		if (typeof c.name === "string") return c.name;
		return [c.name?.firstName, c.name?.lastName].filter(Boolean).join(" ") || "-";
	}

	private formatDate(d?: string): string {
		if (!d) return "-";
		try { return new Date(d).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }); }
		catch { return "-"; }
	}

	// ── Pipeline view ──
	private renderPipeline(): string {
		if (this.opportunities.length === 0) {
			return `<div class="empty-state">No opportunities found${this.space === "demo" ? " — connect a Twenty workspace to see pipeline data" : ""}.</div>`;
		}

		const byStage = new Map<string, Opportunity[]>();
		for (const stage of PIPELINE_STAGES) byStage.set(stage, []);
		for (const opp of this.opportunities) {
			const stage = opp.stage || "INCOMING";
			const list = byStage.get(stage) || byStage.get("INCOMING")!;
			list.push(opp);
		}

		return `<div class="pipeline-grid">
			${PIPELINE_STAGES.map(stage => {
				const opps = byStage.get(stage) || [];
				const total = opps.reduce((s, o) => s + (o.amount?.amountMicros || 0), 0) / 1_000_000;
				const stageClass = stage.toLowerCase().replace(/_/g, "-");
				return `<div class="pipeline-column">
					<div class="pipeline-header stage-${stageClass}">
						<span class="pipeline-stage-name">${STAGE_LABELS[stage] || stage}</span>
						<span class="pipeline-count">${opps.length}</span>
					</div>
					${total > 0 ? `<div class="pipeline-total">${this.formatAmount({ amountMicros: total * 1_000_000, currencyCode: "USD" })}</div>` : ""}
					<div class="pipeline-cards">
						${opps.map(opp => `<div class="pipeline-card" data-collab-id="opp:${opp.id}">
							<div class="card-name">${this.esc(opp.name)}</div>
							<div class="card-amount">${this.formatAmount(opp.amount)}</div>
							${opp.company?.name ? `<div class="card-company">${this.esc(opp.company.name)}</div>` : ""}
							${opp.pointOfContact ? `<div class="card-contact">${this.esc(this.personName(opp.pointOfContact.name))}</div>` : ""}
							${opp.closeDate ? `<div class="card-date">Close: ${this.formatDate(opp.closeDate)}</div>` : ""}
						</div>`).join("")}
					</div>
				</div>`;
			}).join("")}
		</div>`;
	}

	// ── Contacts table ──
	private renderContacts(): string {
		let filtered = this.people;
		if (this.searchQuery.trim()) {
			const q = this.searchQuery.toLowerCase();
			filtered = filtered.filter(p =>
				this.personName(p.name).toLowerCase().includes(q) ||
				(p.email?.primaryEmail || "").toLowerCase().includes(q) ||
				this.companyName(p.company).toLowerCase().includes(q) ||
				(p.city || "").toLowerCase().includes(q)
			);
		}

		if (this.sortColumn) {
			filtered = [...filtered].sort((a, b) => {
				let va = "", vb = "";
				switch (this.sortColumn) {
					case "name": va = this.personName(a.name); vb = this.personName(b.name); break;
					case "email": va = a.email?.primaryEmail || ""; vb = b.email?.primaryEmail || ""; break;
					case "company": va = this.companyName(a.company); vb = this.companyName(b.company); break;
					case "city": va = a.city || ""; vb = b.city || ""; break;
				}
				const cmp = va.localeCompare(vb);
				return this.sortAsc ? cmp : -cmp;
			});
		}

		if (filtered.length === 0) {
			return `<div class="empty-state">${this.searchQuery ? "No contacts match your search." : "No contacts found."}</div>`;
		}

		const sortIcon = (col: string) => this.sortColumn === col ? (this.sortAsc ? " \u25B2" : " \u25BC") : "";

		return `<table class="data-table">
			<thead><tr>
				<th data-sort="name">Name${sortIcon("name")}</th>
				<th data-sort="email">Email${sortIcon("email")}</th>
				<th data-sort="company">Company${sortIcon("company")}</th>
				<th data-sort="city">City${sortIcon("city")}</th>
				<th>Phone</th>
			</tr></thead>
			<tbody>
				${filtered.map(p => `<tr data-collab-id="contact:${p.id}">
					<td class="cell-name">${this.esc(this.personName(p.name))}</td>
					<td class="cell-email">${p.email?.primaryEmail ? this.esc(p.email.primaryEmail) : "-"}</td>
					<td>${this.esc(this.companyName(p.company))}</td>
					<td>${this.esc(p.city || "-")}</td>
					<td>${this.esc(p.phone?.primaryPhoneNumber || "-")}</td>
				</tr>`).join("")}
			</tbody>
		</table>
		<div class="table-footer">${filtered.length} contact${filtered.length !== 1 ? "s" : ""}</div>`;
	}

	// ── Companies table ──
	private renderCompanies(): string {
		let filtered = this.companies;
		if (this.searchQuery.trim()) {
			const q = this.searchQuery.toLowerCase();
			filtered = filtered.filter(c =>
				(c.name || "").toLowerCase().includes(q) ||
				(c.domainName?.primaryLinkUrl || "").toLowerCase().includes(q) ||
				(c.address?.addressCity || "").toLowerCase().includes(q)
			);
		}

		if (this.sortColumn) {
			filtered = [...filtered].sort((a, b) => {
				let va = "", vb = "";
				switch (this.sortColumn) {
					case "name": va = a.name || ""; vb = b.name || ""; break;
					case "domain": va = a.domainName?.primaryLinkUrl || ""; vb = b.domainName?.primaryLinkUrl || ""; break;
					case "city": va = a.address?.addressCity || ""; vb = b.address?.addressCity || ""; break;
					case "employees": return this.sortAsc ? (a.employees || 0) - (b.employees || 0) : (b.employees || 0) - (a.employees || 0);
				}
				const cmp = va.localeCompare(vb);
				return this.sortAsc ? cmp : -cmp;
			});
		}

		if (filtered.length === 0) {
			return `<div class="empty-state">${this.searchQuery ? "No companies match your search." : "No companies found."}</div>`;
		}

		const sortIcon = (col: string) => this.sortColumn === col ? (this.sortAsc ? " \u25B2" : " \u25BC") : "";

		return `<table class="data-table">
			<thead><tr>
				<th data-sort="name">Name${sortIcon("name")}</th>
				<th data-sort="domain">Domain${sortIcon("domain")}</th>
				<th data-sort="employees">Employees${sortIcon("employees")}</th>
				<th data-sort="city">City${sortIcon("city")}</th>
				<th>Country</th>
			</tr></thead>
			<tbody>
				${filtered.map(c => `<tr data-collab-id="company:${c.id}">
					<td class="cell-name">${this.esc(c.name || "-")}</td>
					<td class="cell-domain">${c.domainName?.primaryLinkUrl ? `<a href="${this.esc(c.domainName.primaryLinkUrl)}" target="_blank" rel="noopener">${this.esc(c.domainName.primaryLinkUrl)}</a>` : "-"}</td>
					<td>${c.employees ?? "-"}</td>
					<td>${this.esc(c.address?.addressCity || "-")}</td>
					<td>${this.esc(c.address?.addressCountry || "-")}</td>
				</tr>`).join("")}
			</tbody>
		</table>
		<div class="table-footer">${filtered.length} compan${filtered.length !== 1 ? "ies" : "y"}</div>`;
	}

	// ── Inline Graph ──

	private buildGraphData(): { nodes: CrmGraphNode[]; edges: CrmGraphEdge[] } {
		const nodes: CrmGraphNode[] = [];
		const edges: CrmGraphEdge[] = [];
		const seenEdges = new Set<string>();

		for (const c of this.companies) {
			nodes.push({ id: "co:" + c.id, name: c.name || "Unknown", type: "company" });
		}

		for (const p of this.people) {
			const name = this.personName(p.name);
			const companyId = p.company?.id ? "co:" + p.company.id : undefined;
			nodes.push({ id: "p:" + p.id, name, type: "person", companyId });
			if (companyId) {
				edges.push({ source: "p:" + p.id, target: companyId, type: "works_at" });
			}
		}

		// Cross-org edges from opportunities
		for (const opp of this.opportunities) {
			if (!opp.pointOfContact?.id || !opp.company?.id) continue;
			const person = this.people.find(p => p.id === opp.pointOfContact!.id);
			if (!person?.company?.id || person.company.id === opp.company.id) continue;
			const key = `p:${person.id}->co:${opp.company.id}`;
			if (seenEdges.has(key)) continue;
			seenEdges.add(key);
			edges.push({ source: "p:" + person.id, target: "co:" + opp.company.id, type: "point_of_contact" });
		}

		return { nodes, edges };
	}

	private computeGraphLayout(nodes: CrmGraphNode[], edges: CrmGraphEdge[]): Record<string, { x: number; y: number }> {
		const pos: Record<string, { x: number; y: number }> = {};
		const W = 800, H = 600;
		const cx = W / 2, cy = H / 2;

		const companies = nodes.filter(n => n.type === "company");
		const orbitR = Math.min(W, H) * 0.3;
		const orgCenters: Record<string, { x: number; y: number }> = {};

		companies.forEach((org, i) => {
			const angle = -Math.PI / 2 + (2 * Math.PI * i) / Math.max(companies.length, 1);
			const p = { x: cx + orbitR * Math.cos(angle), y: cy + orbitR * Math.sin(angle) };
			orgCenters[org.id] = p;
			pos[org.id] = { ...p };
		});

		// Fan people around their company
		const peopleByOrg: Record<string, CrmGraphNode[]> = {};
		for (const n of nodes) {
			if (n.type === "person" && n.companyId && orgCenters[n.companyId]) {
				(peopleByOrg[n.companyId] ??= []).push(n);
			}
		}
		for (const [oid, people] of Object.entries(peopleByOrg)) {
			const c = orgCenters[oid];
			const base = Math.atan2(c.y - cy, c.x - cx);
			const spread = Math.PI * 0.8;
			people.forEach((p, i) => {
				const angle = base - spread / 2 + (spread * i) / Math.max(people.length - 1, 1);
				pos[p.id] = { x: c.x + 110 * Math.cos(angle), y: c.y + 110 * Math.sin(angle) };
			});
		}

		// Unlinked nodes near center
		for (const n of nodes) {
			if (!pos[n.id]) {
				pos[n.id] = { x: cx + (Math.random() - 0.5) * W * 0.4, y: cy + (Math.random() - 0.5) * H * 0.4 };
			}
		}

		// Force iterations
		const allIds = nodes.map(n => n.id).filter(id => pos[id]);
		for (let iter = 0; iter < 70; iter++) {
			const force: Record<string, { fx: number; fy: number }> = {};
			for (const id of allIds) force[id] = { fx: 0, fy: 0 };

			// Repulsion
			for (let i = 0; i < allIds.length; i++) {
				for (let j = i + 1; j < allIds.length; j++) {
					const a = pos[allIds[i]], b = pos[allIds[j]];
					let dx = b.x - a.x, dy = b.y - a.y;
					const dist = Math.max(Math.sqrt(dx * dx + dy * dy), 1);
					const repel = 3000 / (dist * dist);
					dx /= dist; dy /= dist;
					force[allIds[i]].fx -= dx * repel;
					force[allIds[i]].fy -= dy * repel;
					force[allIds[j]].fx += dx * repel;
					force[allIds[j]].fy += dy * repel;
				}
			}

			// Edge attraction
			for (const edge of edges) {
				const a = pos[edge.source], b = pos[edge.target];
				if (!a || !b) continue;
				const dx = b.x - a.x, dy = b.y - a.y;
				const dist = Math.sqrt(dx * dx + dy * dy);
				const idealLen = edge.type === "works_at" ? 100 : 200;
				const attract = (dist - idealLen) * 0.01;
				const ux = dx / Math.max(dist, 1), uy = dy / Math.max(dist, 1);
				if (force[edge.source]) { force[edge.source].fx += ux * attract; force[edge.source].fy += uy * attract; }
				if (force[edge.target]) { force[edge.target].fx -= ux * attract; force[edge.target].fy -= uy * attract; }
			}

			// Center gravity
			for (const id of allIds) {
				force[id].fx += (W / 2 - pos[id].x) * 0.002;
				force[id].fy += (H / 2 - pos[id].y) * 0.002;
			}

			// Apply with damping
			const damping = 0.4 * (1 - iter / 70);
			for (const id of allIds) {
				pos[id].x += force[id].fx * damping;
				pos[id].y += force[id].fy * damping;
			}
		}
		return pos;
	}

	private ensureGraphLayout() {
		if (!this.graphLayoutDirty && Object.keys(this.graphPositions).length > 0) return;
		const { nodes, edges } = this.buildGraphData();
		this.graphNodes = nodes;
		this.graphEdges = edges;
		this.graphPositions = this.computeGraphLayout(nodes, edges);
		this.graphLayoutDirty = false;
	}

	private fitGraphView() {
		const svg = this.shadow.getElementById("graph-svg") as SVGSVGElement | null;
		if (!svg) return;
		this.ensureGraphLayout();
		const positions = Object.values(this.graphPositions);
		if (positions.length === 0) return;

		const pad = 60;
		let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
		for (const p of positions) {
			if (p.x < minX) minX = p.x;
			if (p.y < minY) minY = p.y;
			if (p.x > maxX) maxX = p.x;
			if (p.y > maxY) maxY = p.y;
		}
		minX -= 40; minY -= 40; maxX += 40; maxY += 40;

		const rect = svg.getBoundingClientRect();
		const svgW = rect.width || 800;
		const svgH = rect.height || 600;
		const zoom = Math.min((svgW - pad * 2) / Math.max(maxX - minX, 1), (svgH - pad * 2) / Math.max(maxY - minY, 1), 2);
		this.graphZoom = Math.max(0.1, Math.min(zoom, 4));
		this.graphPanX = (svgW / 2) - ((minX + maxX) / 2) * this.graphZoom;
		this.graphPanY = (svgH / 2) - ((minY + maxY) / 2) * this.graphZoom;
		this.updateGraphTransform();
		this.updateGraphZoomDisplay();
	}

	private updateGraphTransform() {
		const g = this.shadow.getElementById("graph-transform");
		if (g) g.setAttribute("transform", `translate(${this.graphPanX},${this.graphPanY}) scale(${this.graphZoom})`);
	}

	private updateGraphZoomDisplay() {
		const el = this.shadow.getElementById("graph-zoom-level");
		if (el) el.textContent = `${Math.round(this.graphZoom * 100)}%`;
	}

	private zoomGraphAt(screenX: number, screenY: number, factor: number) {
		const oldZoom = this.graphZoom;
		const newZoom = Math.max(0.1, Math.min(4, oldZoom * factor));
		this.graphPanX = screenX - (screenX - this.graphPanX) * (newZoom / oldZoom);
		this.graphPanY = screenY - (screenY - this.graphPanY) * (newZoom / oldZoom);
		this.graphZoom = newZoom;
		this.updateGraphTransform();
		this.updateGraphZoomDisplay();
	}

	private updateGraphNodePosition(nodeId: string) {
		const pos = this.graphPositions[nodeId];
		if (!pos) return;

		const g = this.shadow.querySelector(`[data-node-id="${nodeId}"]`) as SVGGElement | null;
		if (!g) return;

		const node = this.graphNodes.find(n => n.id === nodeId);
		if (!node) return;
		const isOrg = node.type === "company";
		const radius = isOrg ? 22 : 12;

		// Update circles (includes selection ring if present)
		g.querySelectorAll("circle").forEach(circle => {
			circle.setAttribute("cx", String(pos.x));
			circle.setAttribute("cy", String(pos.y));
		});

		// Update text
		const texts = g.querySelectorAll("text");
		if (isOrg) {
			if (texts[0]) { texts[0].setAttribute("x", String(pos.x)); texts[0].setAttribute("y", String(pos.y + 4)); }
			if (texts[1]) { texts[1].setAttribute("x", String(pos.x)); texts[1].setAttribute("y", String(pos.y + radius + 13)); }
		} else {
			if (texts[0]) { texts[0].setAttribute("x", String(pos.x)); texts[0].setAttribute("y", String(pos.y + radius + 13)); }
		}

		// Update cluster circle for company nodes
		if (isOrg) {
			const cluster = this.shadow.querySelector(`[data-cluster="${nodeId}"]`) as SVGCircleElement | null;
			if (cluster) {
				cluster.setAttribute("cx", String(pos.x));
				cluster.setAttribute("cy", String(pos.y));
			}
		}

		// Update connected edges
		for (const edge of this.graphEdges) {
			if (edge.source !== nodeId && edge.target !== nodeId) continue;
			const sp = this.graphPositions[edge.source];
			const tp = this.graphPositions[edge.target];
			if (!sp || !tp) continue;
			const line = this.shadow.querySelector(`[data-edge="${edge.source}:${edge.target}"]`) as SVGLineElement | null;
			if (line) {
				line.setAttribute("x1", String(sp.x));
				line.setAttribute("y1", String(sp.y));
				line.setAttribute("x2", String(tp.x));
				line.setAttribute("y2", String(tp.y));
			}
		}
	}

	private renderGraphTab(): string {
		this.ensureGraphLayout();
		const nodes = this.graphNodes;
		const edges = this.graphEdges;

		if (nodes.length === 0) {
			return `<div class="empty-state">No data to graph. Add contacts and companies to see the relationship network.</div>`;
		}

		const positions = this.graphPositions;

		// Color palette for companies
		const palette = ["#6366f1", "#22c55e", "#f59e0b", "#ec4899", "#14b8a6", "#f97316", "#8b5cf6", "#06b6d4"];
		const companyNodes = nodes.filter(n => n.type === "company");
		const orgColors: Record<string, string> = {};
		companyNodes.forEach((org, i) => { orgColors[org.id] = palette[i % palette.length]; });

		// Cluster background circles
		const clustersSvg = companyNodes.map(org => {
			const pos = positions[org.id];
			if (!pos) return "";
			const color = orgColors[org.id] || "#333";
			return `<circle data-cluster="${org.id}" cx="${pos.x}" cy="${pos.y}" r="120" fill="${color}" opacity="0.05" stroke="${color}" stroke-width="1" stroke-opacity="0.15" stroke-dasharray="4 4"/>`;
		}).join("");

		// Edges
		const edgesSvg = edges.map(edge => {
			const sp = positions[edge.source], tp = positions[edge.target];
			if (!sp || !tp) return "";
			if (edge.type === "works_at") {
				return `<line data-edge="${edge.source}:${edge.target}" x1="${sp.x}" y1="${sp.y}" x2="${tp.x}" y2="${tp.y}" style="stroke:var(--rs-text-muted)" stroke-width="1" opacity="0.3"/>`;
			} else {
				return `<line data-edge="${edge.source}:${edge.target}" x1="${sp.x}" y1="${sp.y}" x2="${tp.x}" y2="${tp.y}" stroke="#c084fc" stroke-width="1.5" stroke-dasharray="6 3" opacity="0.6"/>`;
			}
		}).join("");

		// Nodes
		const nodesSvg = nodes.map(node => {
			const pos = positions[node.id];
			if (!pos) return "";
			const isOrg = node.type === "company";
			const color = isOrg ? (orgColors[node.id] || "#22c55e") : "#3b82f6";
			const radius = isOrg ? 22 : 12;
			const isSelected = this.graphSelectedId === node.id;
			const label = this.esc(node.name);
			const truncLabel = label.length > 14 ? label.slice(0, 12) + "\u2026" : label;

			return `<g class="graph-node" data-node-id="${node.id}" style="cursor:pointer">
				${isSelected ? `<circle cx="${pos.x}" cy="${pos.y}" r="${radius + 6}" fill="none" stroke="${color}" stroke-width="2" opacity="0.6"/>` : ""}
				<circle cx="${pos.x}" cy="${pos.y}" r="${radius}" fill="${color}" opacity="${isOrg ? 0.9 : 0.75}" stroke="${isOrg ? color : "none"}" stroke-width="${isOrg ? 2 : 0}" stroke-opacity="0.3"/>
				${isOrg ? `<text x="${pos.x}" y="${pos.y + 4}" fill="#fff" font-size="9" font-weight="600" text-anchor="middle">${truncLabel}</text>` : ""}
				<text x="${pos.x}" y="${pos.y + radius + 13}" style="fill:var(--rs-text-primary)" font-size="${isOrg ? 11 : 10}" font-weight="${isOrg ? 600 : 400}" text-anchor="middle">${label}</text>
			</g>`;
		}).join("");

		return `<div class="graph-container" id="graph-container">
			<svg id="graph-svg" width="100%" height="100%">
				<g id="graph-transform" transform="translate(${this.graphPanX},${this.graphPanY}) scale(${this.graphZoom})">
					<g id="cluster-layer">${clustersSvg}</g>
					<g id="edge-layer">${edgesSvg}</g>
					<g id="node-layer">${nodesSvg}</g>
				</g>
			</svg>
			<div class="graph-zoom-controls">
				<button class="graph-zoom-btn" id="graph-zoom-in" title="Zoom in">+</button>
				<span class="graph-zoom-level" id="graph-zoom-level">${Math.round(this.graphZoom * 100)}%</span>
				<button class="graph-zoom-btn" id="graph-zoom-out" title="Zoom out">&minus;</button>
				<button class="graph-zoom-btn graph-zoom-btn--fit" id="graph-zoom-fit" title="Fit to view">&#x2922;</button>
			</div>
		</div>
		<div class="graph-legend">
			<div class="graph-legend-item"><span class="graph-legend-dot" style="background:#3b82f6"></span> People</div>
			<div class="graph-legend-item"><span class="graph-legend-dot" style="background:#22c55e"></span> Companies</div>
			<div class="graph-legend-item"><svg width="20" height="10"><line x1="0" y1="5" x2="20" y2="5" style="stroke:var(--rs-text-muted)" stroke-width="2"/></svg> Works at</div>
			<div class="graph-legend-item"><svg width="20" height="10"><line x1="0" y1="5" x2="20" y2="5" stroke="#c084fc" stroke-width="2" stroke-dasharray="4 2"/></svg> Cross-org link</div>
		</div>`;
	}

	// ── Delegations tab ──
	private renderDelegations(): string {
		return `
		<div class="delegations-layout">
			<div class="delegations-col">
				<folk-delegation-manager space="${this.space}" auth-url="${this.getAuthUrl()}"></folk-delegation-manager>
			</div>
			<div class="delegations-col">
				<folk-trust-sankey space="${this.space}" auth-url="${this.getAuthUrl()}"></folk-trust-sankey>
			</div>
		</div>`;
	}

	// ── Main render ──
	private render() {
		let content = "";
		if (this.loading) {
			content = `<div class="loading">Loading CRM data...</div>`;
		} else if (this.error) {
			content = `<div class="error">${this.esc(this.error)}</div>`;
		} else {
			switch (this.activeTab) {
				case "pipeline": content = this.renderPipeline(); break;
				case "contacts": content = this.renderContacts(); break;
				case "companies": content = this.renderCompanies(); break;
				case "graph": content = this.renderGraphTab(); break;
				case "delegations": content = this.renderDelegations(); break;
			}
		}

		const showSearch = this.activeTab === "contacts" || this.activeTab === "companies";

		this.shadow.innerHTML = `
		<style>
			:host { display: block; font-family: system-ui, -apple-system, sans-serif; color: var(--rs-text-primary); }
			* { box-sizing: border-box; }

			.crm-header { display: flex; align-items: center; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
			.crm-title { font-size: 15px; font-weight: 600; color: var(--rs-text-primary); }
			.tour-btn {
				font-size: 0.78rem; padding: 4px 10px; border-radius: 8px; border: none;
				background: transparent; color: var(--rs-text-muted); cursor: pointer;
				font-family: inherit; transition: color 0.15s, background 0.15s;
			}
			.tour-btn:hover { color: var(--rs-text-primary); background: var(--rs-bg-surface-raised); }

			.toolbar { display: flex; gap: 8px; margin-bottom: 16px; align-items: center; }
			.search-input {
				border: 1px solid var(--rs-input-border); border-radius: 8px; padding: 8px 12px;
				background: var(--rs-input-bg); color: var(--rs-input-text); font-size: 13px; width: 260px; outline: none;
			}
			.search-input:focus { border-color: var(--rs-primary-hover); }

			.loading { text-align: center; color: var(--rs-text-muted); padding: 60px 20px; font-size: 14px; }
			.error { text-align: center; color: var(--rs-error); padding: 40px 20px; }
			.empty-state { text-align: center; color: var(--rs-text-muted); padding: 60px 20px; font-size: 14px; }

			/* ── Pipeline grid ── */
			.pipeline-grid {
				display: grid;
				grid-template-columns: repeat(${PIPELINE_STAGES.length}, 1fr);
				gap: 12px;
				overflow-x: auto;
			}
			.pipeline-column { min-width: 180px; }
			.pipeline-header {
				padding: 8px 12px; border-radius: 8px 8px 0 0;
				display: flex; align-items: center; justify-content: space-between;
				font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em;
			}
			.stage-incoming { background: rgba(59, 130, 246, 0.15); color: #3b82f6; }
			.stage-meeting { background: rgba(34, 197, 94, 0.15); color: #22c55e; }
			.stage-proposal { background: rgba(245, 158, 11, 0.15); color: #f59e0b; }
			.stage-closed-won { background: rgba(34, 197, 94, 0.2); color: #16a34a; }
			.stage-closed-lost { background: rgba(239, 68, 68, 0.15); color: #ef4444; }
			.pipeline-count {
				background: rgba(255,255,255,0.1); padding: 1px 7px; border-radius: 10px;
				font-size: 11px;
			}
			.pipeline-total { font-size: 12px; color: var(--rs-text-muted); padding: 4px 12px; }
			.pipeline-cards { display: flex; flex-direction: column; gap: 8px; padding-top: 8px; }
			.pipeline-card {
				background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 8px;
				padding: 12px; transition: border-color 0.15s;
			}
			.pipeline-card:hover { border-color: var(--rs-border-strong); }
			.card-name { font-size: 13px; font-weight: 600; color: var(--rs-text-primary); margin-bottom: 4px; }
			.card-amount { font-size: 14px; font-weight: 700; color: var(--rs-primary-hover); margin-bottom: 6px; }
			.card-company { font-size: 12px; color: var(--rs-text-secondary); }
			.card-contact { font-size: 11px; color: var(--rs-text-muted); margin-top: 2px; }
			.card-date { font-size: 11px; color: var(--rs-text-muted); margin-top: 4px; }

			/* ── Data tables ── */
			.data-table {
				width: 100%; border-collapse: collapse;
				background: var(--rs-input-bg); border-radius: 10px; overflow: hidden;
			}
			.data-table th {
				text-align: left; padding: 10px 14px; font-size: 11px; font-weight: 600;
				color: var(--rs-text-muted); text-transform: uppercase; letter-spacing: 0.04em;
				border-bottom: 1px solid var(--rs-border); cursor: pointer; user-select: none;
				white-space: nowrap;
			}
			.data-table th:hover { color: var(--rs-text-secondary); }
			.data-table td {
				padding: 10px 14px; font-size: 13px; border-bottom: 1px solid var(--rs-border-subtle);
				color: var(--rs-text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
				max-width: 250px;
			}
			.data-table tr:hover td { background: var(--rs-bg-hover); }
			.cell-name { font-weight: 500; color: var(--rs-text-primary); }
			.cell-email { color: #93c5fd; }
			.cell-domain a { color: #93c5fd; text-decoration: none; }
			.cell-domain a:hover { text-decoration: underline; }
			.table-footer { font-size: 12px; color: var(--rs-text-muted); padding: 10px 14px; }

			/* ── Graph tab ── */
			.graph-container {
				position: relative; width: 100%; height: 420px;
				background: var(--rs-canvas-bg, #0a0a0f); border: 1px solid var(--rs-border);
				border-radius: 12px; overflow: hidden; cursor: grab;
			}
			.graph-container.grabbing { cursor: grabbing; }
			.graph-container svg { display: block; width: 100%; height: 100%; }
			.graph-node:hover circle { filter: brightness(1.2); }
			.graph-zoom-controls {
				position: absolute; bottom: 12px; right: 12px;
				display: flex; align-items: center; gap: 4px;
				background: var(--rs-bg-surface); border: 1px solid var(--rs-border-strong);
				border-radius: 8px; padding: 4px 6px; z-index: 5;
			}
			.graph-zoom-btn {
				width: 28px; height: 28px; border: none; border-radius: 6px;
				background: transparent; color: var(--rs-text-primary); font-size: 16px;
				cursor: pointer; display: flex; align-items: center; justify-content: center;
			}
			.graph-zoom-btn:hover { background: var(--rs-bg-hover); }
			.graph-zoom-btn--fit { font-size: 14px; }
			.graph-zoom-level { font-size: 11px; color: var(--rs-text-muted); min-width: 36px; text-align: center; }
			.graph-legend { display: flex; gap: 16px; margin-top: 10px; flex-wrap: wrap; }
			.graph-legend-item { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--rs-text-muted); }
			.graph-legend-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }

			/* ── Delegations tab ── */
			.delegations-layout {
				display: grid; grid-template-columns: 1fr 1fr; gap: 20px;
			}
			.delegations-col { min-width: 0; }

			@media (max-width: 900px) {
				.delegations-layout { grid-template-columns: 1fr; }
				.pipeline-grid { grid-template-columns: repeat(2, 1fr); }
			}
			@media (max-width: 600px) {
				.pipeline-grid { grid-template-columns: 1fr; }
				.toolbar { flex-direction: column; align-items: stretch; }
				.search-input { width: 100%; }
				.data-table td, .data-table th { padding: 8px 10px; font-size: 12px; }
				.graph-container { height: 300px; }
			}
		</style>

		<div class="crm-header">
			<span class="crm-title">CRM</span>
			<a href="https://crm.rspace.online" target="_blank" rel="noopener" style="margin-left:auto;font-size:0.78rem;padding:4px 10px;color:var(--link,#60a5fa);text-decoration:none" title="Open full Twenty CRM">Full CRM &rarr;</a>
			<button class="tour-btn" id="btn-tour">Tour</button>
		</div>

		${showSearch ? `<div class="toolbar">
			<input class="search-input" type="text" placeholder="Search..." id="crm-search" value="${this.esc(this.searchQuery)}">
		</div>` : ""}

		<div class="crm-content">${content}</div>
		`;

		this.attachListeners();
		this._tour.renderOverlay();
	}

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

	private attachListeners() {
		// Tour button
		this.shadow.getElementById("btn-tour")?.addEventListener("click", () => this.startTour());

		// Search
		let searchTimeout: any;
		this.shadow.getElementById("crm-search")?.addEventListener("input", (e) => {
			this.searchQuery = (e.target as HTMLInputElement).value;
			clearTimeout(searchTimeout);
			searchTimeout = setTimeout(() => this.render(), 200);
		});

		// Sort columns
		this.shadow.querySelectorAll("[data-sort]").forEach(el => {
			el.addEventListener("click", () => {
				const col = (el as HTMLElement).dataset.sort!;
				if (this.sortColumn === col) {
					this.sortAsc = !this.sortAsc;
				} else {
					this.sortColumn = col;
					this.sortAsc = true;
				}
				this.render();
			});
		});

		// ── Graph interactions ──
		if (this.activeTab === "graph") {
			const container = this.shadow.getElementById("graph-container");
			const svg = this.shadow.getElementById("graph-svg") as SVGSVGElement | null;

			if (svg && container) {
				// Zoom controls
				this.shadow.getElementById("graph-zoom-in")?.addEventListener("click", () => {
					const rect = svg.getBoundingClientRect();
					this.zoomGraphAt(rect.width / 2, rect.height / 2, 1.25);
				});
				this.shadow.getElementById("graph-zoom-out")?.addEventListener("click", () => {
					const rect = svg.getBoundingClientRect();
					this.zoomGraphAt(rect.width / 2, rect.height / 2, 0.8);
				});
				this.shadow.getElementById("graph-zoom-fit")?.addEventListener("click", () => this.fitGraphView());

				// Wheel zoom
				svg.addEventListener("wheel", (e: WheelEvent) => {
					e.preventDefault();
					const rect = svg.getBoundingClientRect();
					this.zoomGraphAt(e.clientX - rect.left, e.clientY - rect.top, 1 - e.deltaY * 0.003);
				}, { passive: false });

				// Pointer down — node drag or canvas pan
				svg.addEventListener("pointerdown", (e: PointerEvent) => {
					if (e.button !== 0) return;
					const target = (e.target as Element).closest("[data-node-id]") as HTMLElement | null;

					if (target) {
						const nodeId = target.dataset.nodeId!;
						this.graphDraggingId = nodeId;
						this.graphDragStartX = e.clientX;
						this.graphDragStartY = e.clientY;
						const pos = this.graphPositions[nodeId];
						if (pos) { this.graphDragNodeStartX = pos.x; this.graphDragNodeStartY = pos.y; }
						svg.setPointerCapture(e.pointerId);
						e.preventDefault();
					} else {
						this.graphIsPanning = true;
						this.graphPanStartX = e.clientX;
						this.graphPanStartY = e.clientY;
						this.graphPanStartPanX = this.graphPanX;
						this.graphPanStartPanY = this.graphPanY;
						container.classList.add("grabbing");
						svg.setPointerCapture(e.pointerId);
						e.preventDefault();
					}
				});

				svg.addEventListener("pointermove", (e: PointerEvent) => {
					if (this.graphDraggingId) {
						const dx = (e.clientX - this.graphDragStartX) / this.graphZoom;
						const dy = (e.clientY - this.graphDragStartY) / this.graphZoom;
						const pos = this.graphPositions[this.graphDraggingId];
						if (pos) {
							pos.x = this.graphDragNodeStartX + dx;
							pos.y = this.graphDragNodeStartY + dy;
							this.updateGraphNodePosition(this.graphDraggingId);
						}
					} else if (this.graphIsPanning) {
						this.graphPanX = this.graphPanStartPanX + (e.clientX - this.graphPanStartX);
						this.graphPanY = this.graphPanStartPanY + (e.clientY - this.graphPanStartY);
						this.updateGraphTransform();
					}
				});

				svg.addEventListener("pointerup", (e: PointerEvent) => {
					if (this.graphDraggingId) {
						const dx = Math.abs(e.clientX - this.graphDragStartX);
						const dy = Math.abs(e.clientY - this.graphDragStartY);
						if (dx < 4 && dy < 4) {
							// Click — toggle selection
							const id = this.graphDraggingId;
							this.graphSelectedId = this.graphSelectedId === id ? null : id;
							this.graphDraggingId = null;
							this.render();
							return;
						}
						this.graphDraggingId = null;
					}
					if (this.graphIsPanning) {
						this.graphIsPanning = false;
						container.classList.remove("grabbing");
					}
				});
			}

			// Fit view on first render
			requestAnimationFrame(() => this.fitGraphView());
		}
	}

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

customElements.define("folk-crm-view", FolkCrmView);
