/* ---- architecture map ---- */
const canvas = $("#canvas"), svg = $("#edgeSvg");
let POSKEY = "fg-pos2";
let positions = {};
let notes = JSON.parse(localStorage.getItem("fg-notes") || "[]");
let zoom = 1;
function loadPositions() { positions = JSON.parse(wsGet(localStorage, POSKEY) || "{}"); }
const savePos = () => localStorage.setItem(wsKey(POSKEY), JSON.stringify(positions));
const saveNotes = () => localStorage.setItem("fg-notes", JSON.stringify(notes));

function renderNodes() {
	$$(".node", canvas).forEach(n => n.remove());
	$$(".lane-box, .gbox", canvas).forEach(n => n.remove());
	$$(".note", canvas).forEach(n => n.remove());
	NODES.forEach(n => {
		const el = document.createElement("div");
		el.className = "node"; el.id = "n-" + n.id;
		el.style.setProperty("--nc", `var(${n.c})`);
		const def = (mapSource === "ai" ? AUTO_LAYOUT[n.id] : LAYOUT[n.id]) || [n.x || 554, n.y || 120];
		const p = positions[n.id] || { x: def[0], y: def[1] };
		el.style.left = p.x + "px"; el.style.top = p.y + "px";
		el.innerHTML = `<h3>${n.t}</h3><p>${envName(n.s)}</p>` +
			(n.env === "staging" ? `<span class="badge staging-only">staging only</span>` : ``);
		if (n.env === "staging" && ENV === "prod") el.style.opacity = 0.45;
		el.addEventListener("pointerdown", ev => startDrag(ev, el, n.id));
		el.addEventListener("click", ev => { if (!dragMoved) openDrawer(n); });
		el.addEventListener("dblclick", ev => { ev.stopPropagation(); zoomToNode(el); });
		el.addEventListener("pointerenter", () => setFocusNode(n.id));
		el.addEventListener("pointerleave", () => setFocusNode(null));
		canvas.appendChild(el);
	});
	renderGroupBoxes();
}
const GROUP_TITLES = { clients: "Clients", edge: "Edge", core: "Core", async: "Async & events", ext: "Data & external", stag: "Staging" };
const GROUP_TINT = { clients: "--aqua", edge: "--blue", core: "--violet", async: "--yellow", ext: "--red", stag: "--orange" };
function renderGroupBoxes() {
	$$(".gbox", canvas).forEach((b) => b.remove());
	const groups = {};
	NODES.forEach((n) => {
		const el = $("#n-" + n.id);
		if (!el) return;
		(groups[n.g] = groups[n.g] || []).push(el);
	});
	Object.entries(groups).forEach(([g, els]) => {
		const x1 = Math.min(...els.map((e) => e.offsetLeft)) - 22;
		const y1 = Math.min(...els.map((e) => e.offsetTop)) - 26;
		const x2 = Math.max(...els.map((e) => e.offsetLeft + e.offsetWidth)) + 22;
		const y2 = Math.max(...els.map((e) => e.offsetTop + e.offsetHeight)) + 22;
		const b = document.createElement("div");
		b.className = "gbox";
		b.style.setProperty("--gc", `var(${GROUP_TINT[g] || "--blue"})`);
		b.style.left = x1 + "px"; b.style.top = y1 + "px";
		b.style.width = (x2 - x1) + "px"; b.style.height = (y2 - y1) + "px";
		b.innerHTML = `<span>${GROUP_TITLES[g] || g}</span>`;
		b.querySelector("span").onclick = () => fitGroup(g);
		canvas.insertBefore(b, canvas.firstChild.nextSibling);
	});
}
let mapFitDone = false;
let detailsOn = localStorage.getItem("me-details") === "1";
function applyDetails() {
	canvas.classList.toggle("clean", !detailsOn);
	$("#detailsBtn").classList.toggle("on", detailsOn);
	$("#detailsBtn").textContent = detailsOn ? "Hide details" : "Details";
	if (mapSource === "ai") { AUTO_LAYOUT = computeAutoLayout(NODES); renderNodes(); }
	setTimeout(() => { drawEdges(); renderGroupBoxes(); }, 30);
}
$("#detailsBtn").onclick = () => {
	detailsOn = !detailsOn;
	localStorage.setItem("me-details", detailsOn ? "1" : "0");
	applyDetails();
};
let dragMoved = false;
let hoverNodeId = null;
function setFocusNode(id) {
	if (flowActive || penMode) return;
	hoverNodeId = id;
	const focus = id || liveDrawerNode?.id;
	NODES.forEach((n) => {
		const el = $("#n-" + n.id);
		if (!el) return;
		const nb = focus && EDGES.some((e) => (e[0] === focus && e[1] === n.id) || (e[1] === focus && e[0] === n.id));
		el.classList.toggle("nb", !!nb);
		el.classList.toggle("dim", !!focus && n.id !== focus && !nb);
	});
	drawEdges();
}
function startDrag(ev, el, nodeId, noteIdx) {
	if (ev.button !== 0) return;
	dragMoved = false;
	el.classList.add("dragging");
	el.setPointerCapture(ev.pointerId);
	const sx = ev.clientX, sy = ev.clientY;
	const ox = parseFloat(el.style.left), oy = parseFloat(el.style.top);
	const move = e => {
		const dx = (e.clientX - sx) / zoom, dy = (e.clientY - sy) / zoom;
		if (Math.abs(dx) + Math.abs(dy) > 3) dragMoved = true;
		el.style.left = Math.max(0, ox + dx) + "px";
		el.style.top = Math.max(0, oy + dy) + "px";
		if (nodeId) drawEdges();
	};
	const up = () => {
		el.classList.remove("dragging");
		el.removeEventListener("pointermove", move);
		el.removeEventListener("pointerup", up);
		if (nodeId) { positions[nodeId] = { x: parseFloat(el.style.left), y: parseFloat(el.style.top) }; savePos(); renderGroupBoxes(); }
	};
	el.addEventListener("pointermove", move);
	el.addEventListener("pointerup", up);
}
$("#resetLayout").onclick = () => { positions = {}; savePos(); renderNodes(); drawEdges(); fitAll(); };
const VIEW = { x: 30, y: 10, z: 1 };
function applyView() {
	VIEW.z = Math.min(2, Math.max(0.3, VIEW.z));
	zoom = VIEW.z;
	canvas.style.transform = `translate(${VIEW.x}px, ${VIEW.y}px) scale(${VIEW.z})`;
}
function zoomAt(factor, cx, cy) {
	const r = $("#mapWrap").getBoundingClientRect();
	const ax = cx != null ? cx - r.left : r.width / 2;
	const ay = cy != null ? cy - r.top : r.height / 2;
	const wx = (ax - VIEW.x) / VIEW.z, wy = (ay - VIEW.y) / VIEW.z;
	VIEW.z = Math.min(2, Math.max(0.3, VIEW.z * factor));
	VIEW.x = ax - wx * VIEW.z;
	VIEW.y = ay - wy * VIEW.z;
	applyView();
}
function nodesBBox(list) {
	const els = list.map((n) => $("#n-" + n.id)).filter(Boolean);
	if (!els.length) return null;
	return {
		x1: Math.min(...els.map((e) => e.offsetLeft)) - 44,
		y1: Math.min(...els.map((e) => e.offsetTop)) - 54,
		x2: Math.max(...els.map((e) => e.offsetLeft + e.offsetWidth)) + 44,
		y2: Math.max(...els.map((e) => e.offsetTop + e.offsetHeight)) + 44,
	};
}
function fitBBox(b) {
	if (!b) return;
	const w = $("#mapWrap");
	VIEW.z = Math.min(1.6, Math.max(0.3, Math.min(w.clientWidth / (b.x2 - b.x1), w.clientHeight / (b.y2 - b.y1))));
	VIEW.x = (w.clientWidth - (b.x2 - b.x1) * VIEW.z) / 2 - b.x1 * VIEW.z;
	VIEW.y = (w.clientHeight - (b.y2 - b.y1) * VIEW.z) / 2 - b.y1 * VIEW.z;
	applyView();
}
function fitAll() { fitBBox(nodesBBox(NODES)); }
function fitGroup(g) { fitBBox(nodesBBox(NODES.filter((n) => n.g === g))); }
function centerOnNode(el, z) {
	if (!el) return;
	if (z) VIEW.z = Math.min(2, Math.max(0.3, z));
	const w = $("#mapWrap");
	VIEW.x = w.clientWidth / 2 - (el.offsetLeft + el.offsetWidth / 2) * VIEW.z;
	VIEW.y = w.clientHeight / 2 - (el.offsetTop + el.offsetHeight / 2) * VIEW.z;
	applyView();
}
function zoomToNode(el) { centerOnNode(el, 1.12); }
$("#zoomIn").onclick = () => zoomAt(1.18);
$("#zoomOut").onclick = () => zoomAt(1 / 1.18);
$("#zoomFit").onclick = fitAll;

function anchor(el) {
	return { x: el.offsetLeft, y: el.offsetTop, w: el.offsetWidth, h: el.offsetHeight,
		cx: el.offsetLeft + el.offsetWidth / 2, cy: el.offsetTop + el.offsetHeight / 2 };
}
function edgePath(a, b) {
	// choose sides based on relative position
	let x1, y1, x2, y2;
	if (Math.abs(b.cx - a.cx) > Math.abs(b.cy - a.cy)) {
		if (b.cx > a.cx) { x1 = a.x + a.w; x2 = b.x; } else { x1 = a.x; x2 = b.x + b.w; }
		y1 = a.cy; y2 = b.cy;
		const mx = (x1 + x2) / 2;
		return { d: `M${x1},${y1} C${mx},${y1} ${mx},${y2} ${x2},${y2}`, lx: (x1 + x2) / 2, ly: (y1 + y2) / 2 - 6 };
	} else {
		if (b.cy > a.cy) { y1 = a.y + a.h; y2 = b.y; } else { y1 = a.y; y2 = b.y + b.h; }
		x1 = a.cx; x2 = b.cx;
		const my = (y1 + y2) / 2;
		return { d: `M${x1},${y1} C${x1},${my} ${x2},${my} ${x2},${y2}`, lx: (x1 + x2) / 2 + 6, ly: (y1 + y2) / 2 };
	}
}
let flowActive = null, flowStep = 0;
function drawEdges() {
	svg.innerHTML = `<defs>
		<marker id="arw" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse"><path d="M0,1L9,5L0,9z" fill="var(--edge)"></path></marker>
		<marker id="arwHi" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M0,1L9,5L0,9z" fill="var(--edge-hi)"></path></marker>
	</defs>`;
	const hiPairs = new Set();
	let pulseIdx = 0;
	const focusId = !flowActive ? (hoverNodeId || liveDrawerNode?.id) : null;
	if (flowActive) {
		const flow = FLOWS[flowActive], steps = flow.steps;
		const addPairs = (A, B) => A.forEach(a => B.forEach(b => { hiPairs.add(a + ">" + b); hiPairs.add(b + ">" + a); }));
		const range = flow.cumulative ? [...Array(flowStep + 1).keys()] : [flowStep];
		range.forEach(i => {
			addPairs(steps[i][0], steps[i][0]);
			if (i > 0) addPairs(steps[i - 1][0], steps[i][0]);
		});
		if (!flow.cumulative && flowStep > 0) addPairs(steps[flowStep - 1][0], steps[flowStep][0]);
	}
	EDGES.forEach(([f, t, lbl, dash]) => {
		const ef = $("#n-" + f), et = $("#n-" + t);
		if (!ef || !et) return;
		const p = edgePath(anchor(ef), anchor(et));
		const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
		path.setAttribute("d", p.d);
		let cls = "edge" + (dash ? " dashed" : "");
		const isHi = flowActive && hiPairs.has(f + ">" + t);
		const isFocus = !flowActive && focusId && (f === focusId || t === focusId);
		if (flowActive) cls += isHi ? (FLOWS[flowActive]?.cumulative ? " hi" : " pulse") : " dim";
		else if (focusId) cls += isFocus ? " pulse" : " dim";
		path.setAttribute("class", cls);
		if (cls.includes("pulse")) { path.style.animationDelay = (pulseIdx * 0.18) + "s"; pulseIdx++; }
		path.setAttribute("marker-end", `url(#${(isHi || isFocus) ? "arwHi" : "arw"})`);
		svg.appendChild(path);
		if (lbl) {
			const txt = document.createElementNS("http://www.w3.org/2000/svg", "text");
			txt.setAttribute("x", p.lx); txt.setAttribute("y", p.ly);
			txt.setAttribute("class", "edge-label" + ((flowActive && hiPairs.has(f + ">" + t)) || (focusId && (f === focusId || t === focusId)) ? " hi" : ""));
			txt.setAttribute("text-anchor", "middle");
			txt.textContent = lbl;
			if ((!flowActive && (!focusId || f === focusId || t === focusId)) || (flowActive && hiPairs.has(f + ">" + t))) svg.appendChild(txt);
		}
	});
}
/* flows */
const flowBtnsEl = $("#flowBtns");
const walkBtn = document.createElement("button");
walkBtn.className = "flow-btn tour";
walkBtn.textContent = "▶ Walkthrough";
walkBtn.onclick = openWalkPick;
function renderFlowBtns() {
	flowBtnsEl.textContent = "";
	const curated = wsCurated();
	if (curated || (mapSource === "ai" && NODES.length)) flowBtnsEl.appendChild(walkBtn);
	if (!curated) return;
	Object.keys(FLOWS).forEach(name => {
		if (FLOWS[name].tour || name === "__walk") return;
		const b = document.createElement("button");
		b.className = "flow-btn";
		b.textContent = name;
		b.onclick = () => startFlow(name, b);
		flowBtnsEl.appendChild(b);
	});
}
renderFlowBtns();
function openWalkPick() {
	const opts = [{ id: "full", t: "Full system", s: "end to end, for anyone", full: true }]
		.concat(NODES.map((n) => ({ id: n.id, t: n.t, s: envName(n.s) })));
	$("#walkOpts").innerHTML = opts.map((o) =>
		`<button class="walk-opt${o.full ? " full" : ""}" data-w="${o.id}" data-l="${o.t}"><b>${o.full ? "▶ " : ""}${o.t}</b><span>${o.s}</span></button>`).join("");
	$("#walkMsg").textContent = "";
	$("#walkPick").classList.remove("hidden");
	$$("#walkOpts .walk-opt").forEach((b) => b.onclick = () => genWalk(b.dataset.w, b.dataset.l));
}
$("#walkPick").addEventListener("click", (e) => { if (e.target === $("#walkPick")) $("#walkPick").classList.add("hidden"); });
async function genWalk(topic, label) {
	$("#walkMsg").textContent = "Generating with AI… about 20 seconds. Cached afterwards until the map changes.";
	try {
		const graph = { nodes: NODES.map(({ id, t, s, g }) => ({ id, t, s, g })), edges: EDGES };
		const r = await fetch(`/api/ai/walkthrough?env=${ENV}`, { method: "POST", body: JSON.stringify({ topic, label, graph }) });
		const d = await r.json();
		if (!d.ok) { $("#walkMsg").textContent = d.detail || "generation failed — try again"; return; }
		FLOWS.__walk = { cumulative: false, label: topic === "full" ? "System walkthrough" : label, steps: d.steps.map((st) => [st.ids, st.text, st.title]) };
		$("#walkPick").classList.add("hidden");
		if (flowActive) stopFlow();
		startFlow("__walk", walkBtn);
	} catch { $("#walkMsg").textContent = "generation failed — is the sync server running?"; }
}
function startFlow(name, btn) {
	if (flowActive === name) return stopFlow();
	flowActive = name; flowStep = 0;
	$$(".flow-btn").forEach(x => x.classList.toggle("active", x === btn));
	$("#flowCaption").classList.add("show");
	updateFlow();
}
function updateFlow() {
	const flow = FLOWS[flowActive], steps = flow.steps;
	const [ids, txt, ttl] = steps[flowStep];
	$("#fcCount").textContent = (flowStep + 1) + " / " + steps.length;
	$("#fcTitle").textContent = ttl || flow.label || flowActive;
	$("#flowStepTxt").innerHTML = txt;
	$("#fcDots").innerHTML = steps.map((_, i) => `<i class="${i <= flowStep ? "on" : ""}"></i>`).join("");
	const active = new Set(flow.cumulative ? steps.slice(0, flowStep + 1).flatMap(s => s[0]) : ids);
	const inAny = new Set(steps.flatMap(s => s[0]));
	$$(".node").forEach(n => {
		const id = n.id.slice(2);
		n.classList.toggle("flow-hi", active.has(id));
		n.classList.toggle("dim", !active.has(id) && !(flow.cumulative && inAny.has(id)));
	});
	drawEdges();
	if (!flow.cumulative) {
		canvas.classList.add("anim");
		const b = nodesBBox(NODES.filter((n) => ids.includes(n.id)));
		if (b) {
			b.x1 -= 90; b.y1 -= 60; b.x2 += 90; b.y2 += 140;
			fitBBox(b);
			if (VIEW.z > 1.05) { VIEW.z = 1.05; const w = $("#mapWrap"); VIEW.x = (w.clientWidth - (b.x2 - b.x1) * VIEW.z) / 2 - b.x1 * VIEW.z; VIEW.y = (w.clientHeight - (b.y2 - b.y1) * VIEW.z) / 2 - b.y1 * VIEW.z; applyView(); }
		}
		setTimeout(() => canvas.classList.remove("anim"), 650);
	} else {
		const el = $("#n-" + ids[0]);
		if (el) centerOnNode(el);
	}
}
function stopFlow() {
	flowActive = null;
	$$(".flow-btn").forEach(x => x.classList.remove("active"));
	$("#flowCaption").classList.remove("show");
	$$(".node").forEach(n => n.classList.remove("flow-hi", "dim"));
	drawEdges();
}
$("#flowNext").onclick = () => { if (flowStep < FLOWS[flowActive].steps.length - 1) { flowStep++; updateFlow(); } };
$("#flowPrev").onclick = () => { if (flowStep > 0) { flowStep--; updateFlow(); } };
$("#flowStop").onclick = stopFlow;
if (localStorage.getItem("me-fc-big") === "1") $("#flowCaption").classList.add("big");
$("#fcSize").onclick = () => {
	const big = $("#flowCaption").classList.toggle("big");
	localStorage.setItem("me-fc-big", big ? "1" : "0");
};
document.addEventListener("keydown", e => {
	if (!flowActive) return;
	if (e.key === "ArrowRight") $("#flowNext").click();
	if (e.key === "ArrowLeft") $("#flowPrev").click();
	if (e.key === "Escape") stopFlow();
});

/* drawer */
function openDrawer(n) {
	$$(".node").forEach(x => x.classList.toggle("sel", x.id === "n-" + n.id));
	$("#dTitle").textContent = n.t;
	$("#dSub").textContent = envName(n.s);
	liveDrawerNode = n;
	let h = `<div class="dsec"><p>${envName(n.d.desc)}</p></div>`;
	h += liveSection(n);
	if (n.d.facts) h += `<div class="dsec" style="--nc:var(${n.c})"><h4>Key facts</h4><ul>` + n.d.facts.map(f => `<li>${envName(f)}</li>`).join("") + `</ul></div>`;
	if (n.d.cfg) {
		h += `<div class="dsec"><h4>Configuration</h4>` + n.d.cfg.map(([k, v]) =>
			`<div class="live-row"><span class="lk">${k}</span><span class="lv">${v}</span></div>`).join("") +
			`<p class="cfg-note">Read-only. Source of truth: the infrastructure code.</p></div>`;
	}
	if (wsCurated() && NODE_LINKS[n.id]) {
		h += `<div class="dsec"><h4>Open</h4><div class="dlinks">` + NODE_LINKS[n.id].map(([l, u]) =>
			`<a href="${envName(u)}" target="_blank" rel="noopener">${l}</a>`).join("") + `</div></div>`;
	}
	if (n.d.logs) h += `<div class="dsec"><h4>Logs</h4>` + n.d.logs.map(([c, note]) => `<div class="cmd">${envName(c)}<button class="copy">copy</button></div>` + (note ? `<p style="font-size:11.5px;color:var(--ink-3)">${note}</p>` : "")).join("") + `</div>`;
	if (n.d.fixes) h += `<div class="dsec" style="--nc:var(--warning)"><h4>Quick fixes</h4><ul>` + n.d.fixes.map(f => `<li>${envName(f)}</li>`).join("") + `</ul></div>`;
	$("#dBody").innerHTML = h;
	bindCopies($("#dBody"));
	$("#drawer").classList.add("open");
}
function closeDrawer() {
	$("#drawer").classList.remove("open");
	$$(".node").forEach(x => x.classList.remove("sel"));
	liveDrawerNode = null;
	if (typeof setFocusNode === "function") setFocusNode(null);
}
$("#drawerClose").onclick = closeDrawer;
function bindCopies(root) {
	$$(".copy", root).forEach(b => b.onclick = e => {
		e.stopPropagation();
		navigator.clipboard.writeText(b.parentElement.textContent.replace(/copy$/, "").trim());
		b.textContent = "✓"; setTimeout(() => b.textContent = "copy", 900);
	});
}

