package org.kne.cloud.network.klalb.web;

import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;

import com.sun.net.httpserver.*;
import com.google.gson.*;

import org.kne.cloud.clock.HighAccuracyClock;
import org.kne.cloud.network.MultiProtocolSocketAddress;
import org.kne.cloud.network.ipv6.IPv6Address;
import org.kne.cloud.network.ipv6.IPv6AddressGroup;
import org.kne.cloud.network.ipv6.IPv6NetworkLink;
import org.kne.cloud.network.ipv6.RouteItem;
import org.kne.cloud.network.klalb.*;
import org.kne.cloud.network.monitor.LinkStatus;
import org.kne.cloud.network.srv6.KLALBNodeInformation;
import org.kne.cloud.network.srv6.KLALBRoutingProtocol;
import org.kne.cloud.network.srv6.KLALBRoutingProtocol.LinkDirection;
import org.kne.cloud.network.srv6.KLALBRoutingProtocolAPIClient;
import org.kne.cloud.network.srv6.NeighborInfo;
import org.kne.cloud.network.srv6.RouterInfo;
import org.kne.cloud.network.srv6.SRv6Router;

/**
 * KLALB Web Server providing REST API, SSE live event stream,
 * and static dashboard hosting.
 */
public class KLALBWebServer {
	private final KLALBProxySystem proxySystem;
	private final MultiProtocolSocketAddress listen;
	private HttpServer server;
	private final Gson gson;
	private final ScheduledExecutorService sseExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
		Thread t = new Thread(r, "KLALB-Web-SSE");
		t.setDaemon(true);
		return t;
	});
	private final Set<HttpExchange> sseClients = Collections.newSetFromMap(new ConcurrentHashMap<>());
	private final AtomicBoolean running = new AtomicBoolean(false);

	public KLALBWebServer(KLALBProxySystem proxySystem, int port) {
		this(proxySystem, new MultiProtocolSocketAddress("http", "0.0.0.0", port));
	}

	public KLALBWebServer(KLALBProxySystem proxySystem, MultiProtocolSocketAddress listen) {
		this.proxySystem = proxySystem;
		this.listen = listen;
		this.gson = proxySystem.getGson();
	}

	public synchronized void start() throws IOException {
		if (running.get()) return;

		InetSocketAddress socketAddress = "0.0.0.0".equals(listen.getHost())
				? new InetSocketAddress(listen.getPort()) : listen.getSocketAddress();
		server = HttpServer.create(socketAddress, 0);
		server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());

		// API Contexts
		server.createContext("/api/status", this::handleStatus);
		server.createContext("/api/events", this::handleEvents);
		server.createContext("/api/links", this::handleLinks);
		server.createContext("/api/links/action", this::handleLinkAction);
		server.createContext("/api/links/reconnect", this::handleReconnectAll);
		server.createContext("/api/routing-table", this::handleRoutingTable);
		server.createContext("/api/nodes", this::handleNodes);
		server.createContext("/api/node-info", this::handleNodeInfo);
		server.createContext("/api/config", this::handleConfig);

		// Static Files / SPA Fallback Handler
		server.createContext("/", this::handleStatic);

		server.start();
		running.set(true);
		startSseBroadcaster();

		System.out.println("KLALB Web Dashboard started at " + listen);
	}

	public synchronized void stop() {
		if (!running.get()) return;
		running.set(false);
		sseExecutor.shutdownNow();
		for (HttpExchange client : sseClients) {
			try {
				client.close();
			} catch (Exception ignored) {}
		}
		sseClients.clear();
		if (server != null) {
			server.stop(1);
			server = null;
		}
		System.out.println("KLALB Web Dashboard stopped.");
	}

	public boolean isRunning() {
		return running.get();
	}

	public int getPort() {
		return listen.getPort();
	}

	private void setCorsHeaders(HttpExchange exchange) {
		Headers headers = exchange.getResponseHeaders();
		headers.set("Access-Control-Allow-Origin", "*");
		headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
		headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept");
	}

	private boolean handleCorsPreflight(HttpExchange exchange) throws IOException {
		setCorsHeaders(exchange);
		if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) {
			exchange.sendResponseHeaders(204, -1);
			exchange.close();
			return true;
		}
		return false;
	}

	private void sendJsonResponse(HttpExchange exchange, int statusCode, Object data) throws IOException {
		setCorsHeaders(exchange);
		String json;
		if (data instanceof JsonElement) {
			// gson-2.1 的 toJson(Object) 会按运行时类型 JsonObject 反射序列化出内部的 members 字段，
			// 必须走 JsonElement 重载（或 toString）直接输出 JSON 树
			json = ((JsonElement) data).toString();
		} else {
			json = gson.toJson(data);
		}
		byte[] bytes = json.getBytes(StandardCharsets.UTF_8);
		exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
		exchange.sendResponseHeaders(statusCode, bytes.length);
		try (OutputStream os = exchange.getResponseBody()) {
			os.write(bytes);
		}
	}

	private void sendError(HttpExchange exchange, int statusCode, String message) throws IOException {
		JsonObject error = new JsonObject();
		error.addProperty("error", message);
		sendJsonResponse(exchange, statusCode, error);
	}

	private String readRequestBody(HttpExchange exchange) throws IOException {
		try (InputStream is = exchange.getRequestBody()) {
			return new String(is.readAllBytes(), StandardCharsets.UTF_8);
		}
	}

	// -------------------------------------------------------------
	// API Handlers
	// -------------------------------------------------------------

	private JsonObject getStatusJson() {
		JsonObject root = new JsonObject();
		KLALBController kc = proxySystem.getKlalbController();
		if (kc == null) {
			root.addProperty("status", "not_ready");
			return root;
		}

		root.addProperty("version", CONST.klalbver);
		root.addProperty("title", CONST.klalb);

		IPv6AddressGroup self = kc.getSelf();
		root.addProperty("address", self != null ? self.getAddress().toString() : "");

		SRv6Router router = kc.getIpv6Router();
		if (router != null && router.getKlalbRouteProtol() != null) {
			root.addProperty("onlineDevices", router.getKlalbRouteProtol().getDevicesFound());
			root.addProperty("deviceName", router.getDeviceName() != null ? router.getDeviceName() : "");
		} else {
			root.addProperty("onlineDevices", 0);
			root.addProperty("deviceName", "");
		}

		KLALBControllerConfigItem configItem = proxySystem.getControllerConfig();
		if (configItem != null) {
			root.addProperty("deviceDescription", configItem.getDeviceDescription() != null ? configItem.getDeviceDescription() : "");
			root.addProperty("enableTUN", configItem.isEnableTUN());
			root.addProperty("tunName", configItem.isEnableTUN() && configItem.getTUNName() != null ? configItem.getTUNName() : "");
			root.addProperty("congestionAlgorithm", configItem.getCongestionAlgorithm() != null ? configItem.getCongestionAlgorithm() : "");
			root.addProperty("performanceStrategy", configItem.getPerformanceStrategy() != null ? configItem.getPerformanceStrategy() : "");
		}

		// Clock
		HighAccuracyClock hac = kc.getClock();
		if (hac != null) {
			long delta = hac.getFrequency() - 1000000000;
			double ppm = delta / 1000.0;
			root.addProperty("timeStr", hac.toString2());
			root.addProperty("timePpm", ppm);
		}

		// Metrics
		JsonObject metrics = new JsonObject();
		if (kc.getLinkMonitor() != null) {
			metrics.addProperty("upSpeed", kc.getLinkMonitor().getOutSpeed());
			metrics.addProperty("downSpeed", kc.getLinkMonitor().getInSpeed());
			metrics.addProperty("upSpeedMax", kc.getLinkMonitor().getOutSpeedMax());
			metrics.addProperty("downSpeedMax", kc.getLinkMonitor().getInSpeedMax());
			metrics.addProperty("upPPS", kc.getLinkMonitor().getOutPPS());
			metrics.addProperty("downPPS", kc.getLinkMonitor().getInPPS());
			metrics.addProperty("upPPSMax", kc.getLinkMonitor().getOutPPSMax());
			metrics.addProperty("downPPSMax", kc.getLinkMonitor().getInPPSMax());
			metrics.addProperty("upTraffic", kc.getLinkMonitor().getOutTraffic());
			metrics.addProperty("downTraffic", kc.getLinkMonitor().getInTraffic());
		}

		if (kc.getDatatMonitor() != null) {
			metrics.addProperty("dataUpSpeed", kc.getDatatMonitor().getOutSpeed());
			metrics.addProperty("dataDownSpeed", kc.getDatatMonitor().getInSpeed());
			metrics.addProperty("dataUpPPS", kc.getDatatMonitor().getOutPPS());
			metrics.addProperty("dataDownPPS", kc.getDatatMonitor().getInPPS());
			metrics.addProperty("dataUpTraffic", kc.getDatatMonitor().getOutTraffic());
			metrics.addProperty("dataDownTraffic", kc.getDatatMonitor().getInTraffic());
		}

		if (router != null) {
			metrics.addProperty("backplaneDelay", router.getBackplaneTime());
			metrics.addProperty("backplanePPS", router.getBackplanePPS());
			metrics.addProperty("backplanePPSMax", router.getBackplanePPSMax());
			metrics.addProperty("backplaneECNRate", router.getECNRate());
			metrics.addProperty("backplaneLossRate", router.getLossRate());
		}
		root.add("metrics", metrics);

		return root;
	}

	private void handleStatus(HttpExchange exchange) throws IOException {
		if (handleCorsPreflight(exchange)) return;
		if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
			sendError(exchange, 405, "Method not allowed");
			return;
		}
		sendJsonResponse(exchange, 200, getStatusJson());
	}

	private JsonArray getLinksJson() {
		JsonArray array = new JsonArray();
		KLALBController kc = proxySystem.getKlalbController();
		if (kc == null) return array;

		List<IPv6NetworkLink> linesList = kc.getLines();
		synchronized (linesList) {
			for (IPv6NetworkLink link : linesList) {
				if (!(link instanceof KLALBRemoteLink)) continue;
				KLALBRemoteLink remoteLink = (KLALBRemoteLink) link;

				JsonObject obj = new JsonObject();
				obj.addProperty("name", remoteLink.getName());
				obj.addProperty("state", LinkStatus.stateToString(remoteLink.getState()));
				obj.addProperty("stateCode", remoteLink.getState());

				MultiProtocolSocketAddress mpsa = remoteLink.getSocketAddress();
				obj.addProperty("socketAddress", mpsa != null ? mpsa.toString() : "");

				IPv6AddressGroup vaddr = remoteLink.getRemoteVaddr();
				obj.addProperty("vaddr", vaddr != null ? vaddr.getAddress().toString() : "");

				if (remoteLink.getMonitor() != null) {
					obj.addProperty("upSpeed", remoteLink.getMonitor().getOutSpeed());
					obj.addProperty("downSpeed", remoteLink.getMonitor().getInSpeed());
					obj.addProperty("upPPS", remoteLink.getMonitor().getOutPPS());
					obj.addProperty("downPPS", remoteLink.getMonitor().getInPPS());
					obj.addProperty("upTraffic", remoteLink.getMonitor().getOutTraffic());
					obj.addProperty("downTraffic", remoteLink.getMonitor().getInTraffic());
					obj.addProperty("upDelay", remoteLink.getMonitor().getOutDelay());
					obj.addProperty("downDelay", remoteLink.getMonitor().getInDelay());
					obj.addProperty("upDelayMin", remoteLink.getMonitor().getOutDelayMin());
					obj.addProperty("downDelayMin", remoteLink.getMonitor().getInDelayMin());
					obj.addProperty("upJitter", remoteLink.getMonitor().getOutJitter());
					obj.addProperty("downJitter", remoteLink.getMonitor().getInJitter());
				}
				array.add(obj);
			}
		}
		return array;
	}

	private void handleLinks(HttpExchange exchange) throws IOException {
		if (handleCorsPreflight(exchange)) return;
		String method = exchange.getRequestMethod();
		KLALBController kc = proxySystem.getKlalbController();

		if ("GET".equalsIgnoreCase(method)) {
			sendJsonResponse(exchange, 200, getLinksJson());
		} else if ("POST".equalsIgnoreCase(method)) {
			String body = readRequestBody(exchange);
			try {
				JsonObject req = new JsonParser().parse(body).getAsJsonObject();
				String address = req.get("address").getAsString().trim();
				if (address.isEmpty()) {
					sendError(exchange, 400, "Address is required");
					return;
				}
				MultiProtocolSocketAddress mpsa = new MultiProtocolSocketAddress(address);
				List<KLALBRemoteLink> added = kc.addRemoteLines(mpsa);
				JsonObject resp = new JsonObject();
				resp.addProperty("success", !added.isEmpty());
				resp.addProperty("count", added.size());
				sendJsonResponse(exchange, 200, resp);
			} catch (Exception e) {
				sendError(exchange, 400, "Failed to add link: " + e.getMessage());
			}
		} else if ("DELETE".equalsIgnoreCase(method)) {
			String query = exchange.getRequestURI().getQuery();
			String address = null;
			if (query != null && query.startsWith("address=")) {
				address = URLDecoder.decode(query.substring(8), StandardCharsets.UTF_8);
			} else {
				String body = readRequestBody(exchange);
				if (!body.isEmpty()) {
					try {
						JsonObject req = new JsonParser().parse(body).getAsJsonObject();
						if (req.has("address")) {
							address = req.get("address").getAsString();
						}
					} catch (Exception ignored) {}
				}
			}

			if (address == null || address.trim().isEmpty()) {
				sendError(exchange, 400, "Address parameter required");
				return;
			}

			try {
				MultiProtocolSocketAddress mpsa = new MultiProtocolSocketAddress(address.trim());
				List<KLALBRemoteLink> removed = kc.removeRemoteLines(mpsa);
				JsonObject resp = new JsonObject();
				resp.addProperty("success", !removed.isEmpty());
				resp.addProperty("count", removed.size());
				sendJsonResponse(exchange, 200, resp);
			} catch (Exception e) {
				sendError(exchange, 400, "Failed to remove link: " + e.getMessage());
			}
		} else {
			sendError(exchange, 405, "Method not allowed");
		}
	}

	private void handleLinkAction(HttpExchange exchange) throws IOException {
		if (handleCorsPreflight(exchange)) return;
		if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) {
			sendError(exchange, 405, "Method not allowed");
			return;
		}

		String body = readRequestBody(exchange);
		try {
			JsonObject req = new JsonParser().parse(body).getAsJsonObject();
			String action = req.get("action").getAsString();
			String address = req.has("address") ? req.get("address").getAsString() : null;

			KLALBController kc = proxySystem.getKlalbController();
			KLALBRemoteLink target = null;
			if (address != null) {
				for (IPv6NetworkLink link : kc.getLines()) {
					if (link instanceof KLALBRemoteLink rl) {
						if (rl.getName().equalsIgnoreCase(address) ||
							(rl.getSocketAddress() != null && rl.getSocketAddress().toString().equalsIgnoreCase(address))) {
							target = rl;
							break;
						}
					}
				}
			}

			JsonObject resp = new JsonObject();
			if (target != null) {
				switch (action) {
					case "reconnect" -> {
						target.reconnectImmediately();
						resp.addProperty("success", true);
					}
					case "disconnect" -> {
						target.dislink();
						resp.addProperty("success", true);
					}
					case "remove" -> {
						MultiProtocolSocketAddress mtar = target.getSocketAddress();
						if (mtar != null) {
							kc.removeRemoteLines(mtar);
						} else {
							target.close();
						}
						resp.addProperty("success", true);
					}
					default -> {
						sendError(exchange, 400, "Unknown action: " + action);
						return;
					}
				}
			} else {
				sendError(exchange, 404, "Target link not found: " + address);
				return;
			}
			sendJsonResponse(exchange, 200, resp);
		} catch (Exception e) {
			sendError(exchange, 400, "Failed to execute link action: " + e.getMessage());
		}
	}

	private void handleReconnectAll(HttpExchange exchange) throws IOException {
		if (handleCorsPreflight(exchange)) return;
		if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) {
			sendError(exchange, 405, "Method not allowed");
			return;
		}
		KLALBController kc = proxySystem.getKlalbController();
		if (kc != null) {
			kc.reconnectImmediately();
			JsonObject resp = new JsonObject();
			resp.addProperty("success", true);
			sendJsonResponse(exchange, 200, resp);
		} else {
			sendError(exchange, 500, "Controller not ready");
		}
	}

	private void handleRoutingTable(HttpExchange exchange) throws IOException {
		if (handleCorsPreflight(exchange)) return;
		if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
			sendError(exchange, 405, "Method not allowed");
			return;
		}

		KLALBController kc = proxySystem.getKlalbController();
		JsonArray routesArray = new JsonArray();
		if (kc != null && kc.getIpv6Router() != null) {
			List<RouteItem> list = new ArrayList<>(kc.getIpv6Router().getCurrentRouteTabel());
			Collections.sort(list);
			for (RouteItem item : list) {
				JsonObject obj = new JsonObject();
				obj.addProperty("destination", item.getDestination() != null ? item.getDestination().toString() : "");
				obj.addProperty("protocol", item.getProto());
				obj.addProperty("preference", item.getPre());
				obj.addProperty("cost", item.getCost());
				obj.addProperty("flag", item.getFlag());
				obj.addProperty("nexthop", item.getNexthop() != null ? item.getNexthop().toString() : "");
				obj.addProperty("interface", item.getDestlink() != null ? item.getDestlink().getName() : "");
				routesArray.add(obj);
			}
		}
		sendJsonResponse(exchange, 200, routesArray);
	}

	private KLALBRoutingProtocolAPIClient nodeInfoClient;

	private synchronized KLALBRoutingProtocolAPIClient getNodeInfoClient(KLALBController kc) {
		if (nodeInfoClient == null) {
			nodeInfoClient = new KLALBRoutingProtocolAPIClient(kc.getIpv6Router().getKlalbRouteProtol());
		}
		return nodeInfoClient;
	}

	private void handleNodeInfo(HttpExchange exchange) throws IOException {
		if (handleCorsPreflight(exchange)) return;
		if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
			sendError(exchange, 405, "Method not allowed");
			return;
		}

		Map<String, String> params = parseQuery(exchange.getRequestURI().getRawQuery());
		String addressStr = params.get("address");
		if (addressStr == null || addressStr.trim().isEmpty()) {
			sendError(exchange, 400, "address parameter is required");
			return;
		}

		KLALBController kc = proxySystem.getKlalbController();
		if (kc == null || kc.getIpv6Router() == null || kc.getIpv6Router().getKlalbRouteProtol() == null) {
			sendError(exchange, 503, "Controller not ready");
			return;
		}

		IPv6Address target;
		try {
			target = new IPv6Address((Inet6Address) InetAddress.getByName(addressStr.trim()));
		} catch (Exception e) {
			sendError(exchange, 400, "Invalid IPv6 address: " + addressStr);
			return;
		}

		SRv6Router router = kc.getIpv6Router();
		boolean isSelf = router.getLocator().getAddress().equals(target);

		JsonObject resp = new JsonObject();
		resp.addProperty("address", target.toString());

		if (isSelf) {
			// 本机：描述直接取本地控制器配置
			KLALBControllerConfigItem cfg = proxySystem.getControllerConfig();
			resp.addProperty("isSelf", true);
			String dname = router.getDeviceName() != null ? router.getDeviceName()
					: (cfg != null && cfg.getDeviceName() != null ? cfg.getDeviceName() : "");
			resp.addProperty("deviceName", dname);
			resp.addProperty("deviceDescription",
					cfg != null && cfg.getDeviceDescription() != null ? cfg.getDeviceDescription() : "");
			JsonArray lines = new JsonArray();
			for (MultiProtocolSocketAddress mpsa : kc.getExternalEndpoints()) {
				lines.add(new JsonPrimitive(mpsa.toString()));
			}
			resp.add("openLines", lines);
			sendJsonResponse(exchange, 200, resp);
			return;
		}

		// 远端节点：经 SRv6 虚拟网络发送完整节点信息查询（异步回调，限时等待）
		resp.addProperty("isSelf", false);
		CompletableFuture<KLALBNodeInformation> future = new CompletableFuture<>();
		try {
			getNodeInfoClient(kc).requestNodeInfoFull(
					new InetSocketAddress(target.toInet6Address(), KLALBRoutingProtocol.DEFAULT_PORT),
					info -> future.complete(info));

			KLALBNodeInformation info;
			try {
				info = future.get(3, TimeUnit.SECONDS);
			} catch (TimeoutException te) {
				info = null;
			}

			String dname = info != null && info.getDeviceName() != null && !info.getDeviceName().isEmpty()
					? info.getDeviceName()
					: router.getKlalbRouteProtol().getDeviceName(target);
			resp.addProperty("deviceName", dname != null ? dname : "");
			resp.addProperty("deviceDescription",
					info != null && info.getDeviceDescription() != null ? info.getDeviceDescription() : "");
			resp.addProperty("reachable", info != null);
			JsonArray lines = new JsonArray();
			if (info != null && info.getOpenLines() != null) {
				for (MultiProtocolSocketAddress mpsa : info.getOpenLines()) {
					lines.add(new JsonPrimitive(mpsa.toString()));
				}
			}
			resp.add("openLines", lines);
		} catch (Exception e) {
			resp.addProperty("deviceName", "");
			resp.addProperty("deviceDescription", "");
			resp.addProperty("reachable", false);
			resp.add("openLines", new JsonArray());
		}
		sendJsonResponse(exchange, 200, resp);
	}

	private Map<String, String> parseQuery(String rawQuery) {
		Map<String, String> params = new HashMap<>();
		if (rawQuery == null || rawQuery.isEmpty()) return params;
		for (String pair : rawQuery.split("&")) {
			int idx = pair.indexOf('=');
			if (idx <= 0) continue;
			try {
				params.put(URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8),
						URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8));
			} catch (Exception ignored) {}
		}
		return params;
	}

	private void handleNodes(HttpExchange exchange) throws IOException {
		if (handleCorsPreflight(exchange)) return;
		if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
			sendError(exchange, 405, "Method not allowed");
			return;
		}

		KLALBController kc = proxySystem.getKlalbController();
		JsonObject result = new JsonObject();
		JsonArray nodesArray = new JsonArray();
		JsonArray edgesArray = new JsonArray();

		if (kc != null && kc.getIpv6Router() != null && kc.getIpv6Router().getKlalbRouteProtol() != null) {
			KLALBRoutingProtocol rproto = kc.getIpv6Router().getKlalbRouteProtol();
			Map<IPv6Address, Long> addrs = rproto.getAddresses();
			IPv6Address selfAddr = kc.getIpv6Router().getLocator().getAddress();

			if (addrs != null) {
				for (IPv6Address addr : addrs.keySet()) {
					JsonObject nodeObj = new JsonObject();
					nodeObj.addProperty("id", addr.toString());
					nodeObj.addProperty("address", addr.toString());
					nodeObj.addProperty("compressedAddress", addr.toCompressedString());
					nodeObj.addProperty("isSelf", addr.equals(selfAddr));
					String dname = rproto.getDeviceName(addr);
					nodeObj.addProperty("deviceName", dname != null ? dname : "");
					nodesArray.add(nodeObj);
				}
			}

			Map<IPv6Address, RouterInfo> netmap = rproto.getNetmap();
			if (netmap != null) {
				Set<String> seenEdges = new HashSet<>();
				for (Map.Entry<IPv6Address, RouterInfo> entry : netmap.entrySet()) {
					IPv6Address from = entry.getKey();
					RouterInfo info = entry.getValue();
					if (info != null && info.getNeighborAddresses() != null) {
						for (NeighborInfo nb : info.getNeighborAddresses()) {
							if (nb != null && nb.getLocator() != null) {
								String to = nb.getLocator().getAddress().toString();
								String edgeKey = from.toString().compareTo(to) < 0
										? from.toString() + "->" + to
										: to + "->" + from.toString();
								if (!seenEdges.contains(edgeKey)) {
									seenEdges.add(edgeKey);
									JsonObject edgeObj = new JsonObject();
									edgeObj.addProperty("source", from.toString());
									edgeObj.addProperty("target", to);
									edgeObj.addProperty("delay", nb.getUploadDelay());
									edgeObj.addProperty("cost", nb.getUploadDelayMin());
									edgesArray.add(edgeObj);
								}
							}
						}
					}
				}
			}
		}

		result.add("nodes", nodesArray);
		result.add("edges", edgesArray);
		sendJsonResponse(exchange, 200, result);
	}

	private void handleConfig(HttpExchange exchange) throws IOException {
		if (handleCorsPreflight(exchange)) return;
		String method = exchange.getRequestMethod();

		if ("GET".equalsIgnoreCase(method)) {
			KLALBControllerConfigItem config = proxySystem.getControllerConfig();
			if (config != null) {
				sendJsonResponse(exchange, 200, config);
			} else {
				sendError(exchange, 404, "Configuration not found");
			}
		} else if ("POST".equalsIgnoreCase(method) || "PUT".equalsIgnoreCase(method)) {
			String body = readRequestBody(exchange);
			try {
				JsonObject json = new JsonParser().parse(body).getAsJsonObject();
				KLALBControllerConfigItem current = proxySystem.getControllerConfig();
				if (current != null) {
					if (json.has("deviceName") && !json.get("deviceName").isJsonNull()) {
						current.setDeviceName(json.get("deviceName").getAsString());
					} else if (json.has("DeviceName") && !json.get("DeviceName").isJsonNull()) {
						current.setDeviceName(json.get("DeviceName").getAsString());
					}

					if (json.has("deviceDescription") && !json.get("deviceDescription").isJsonNull()) {
						current.setDeviceDescription(json.get("deviceDescription").getAsString());
					} else if (json.has("DeviceDescription") && !json.get("DeviceDescription").isJsonNull()) {
						current.setDeviceDescription(json.get("DeviceDescription").getAsString());
					}

					if (json.has("language") && !json.get("language").isJsonNull()) {
						current.setLanguage(json.get("language").getAsString());
					}

					if (json.has("virtualAddress") && !json.get("virtualAddress").isJsonNull()) {
						current.setVirtualAddress(json.get("virtualAddress").getAsString());
					} else if (json.has("VirtualAddress") && !json.get("VirtualAddress").isJsonNull()) {
						current.setVirtualAddress(json.get("VirtualAddress").getAsString());
					}

					if (json.has("virtualASN") && !json.get("virtualASN").isJsonNull()) {
						current.setVirtualASN(json.get("virtualASN").getAsLong());
					} else if (json.has("VirtualASN") && !json.get("VirtualASN").isJsonNull()) {
						current.setVirtualASN(json.get("VirtualASN").getAsLong());
					}

					if (json.has("virtualSocketName") && !json.get("virtualSocketName").isJsonNull()) {
						current.setVirtualSocketName(json.get("virtualSocketName").getAsString());
					} else if (json.has("VirtualSocketName") && !json.get("VirtualSocketName").isJsonNull()) {
						current.setVirtualSocketName(json.get("VirtualSocketName").getAsString());
					}

					if (json.has("enableTUN")) {
						current.setEnableTUN(json.get("enableTUN").getAsBoolean());
					} else if (json.has("EnableTUN")) {
						current.setEnableTUN(json.get("EnableTUN").getAsBoolean());
					}

					if (json.has("tunName") && !json.get("tunName").isJsonNull()) {
						current.setTUNName(json.get("tunName").getAsString());
					} else if (json.has("TUNName") && !json.get("TUNName").isJsonNull()) {
						current.setTUNName(json.get("TUNName").getAsString());
					}

					if (json.has("webUI")) {
						current.setWebUI(json.get("webUI").getAsBoolean());
					}

					if (json.has("webListen") && !json.get("webListen").isJsonNull()) {
						current.setWebListen(new MultiProtocolSocketAddress(json.get("webListen").getAsString()));
					} else if (json.has("webPort")) {
						current.setWebListen(new MultiProtocolSocketAddress("http", "0.0.0.0", json.get("webPort").getAsInt()));
					}

					if (json.has("nogui")) {
						current.setNogui(json.get("nogui").getAsBoolean());
					}

					if (json.has("tcpListen") && !json.get("tcpListen").isJsonNull()) {
						current.setTCPListen(new MultiProtocolSocketAddress(json.get("tcpListen").getAsString()));
					} else if (json.has("TCPListen") && !json.get("TCPListen").isJsonNull()) {
						current.setTCPListen(new MultiProtocolSocketAddress(json.get("TCPListen").getAsString()));
					}

					if (json.has("udpListen") && !json.get("udpListen").isJsonNull()) {
						current.setUDPListen(new MultiProtocolSocketAddress(json.get("udpListen").getAsString()));
					} else if (json.has("UDPListen") && !json.get("UDPListen").isJsonNull()) {
						current.setUDPListen(new MultiProtocolSocketAddress(json.get("UDPListen").getAsString()));
					}

					if (json.has("externalEndpoints") || json.has("ExternalEndpoints") || json.has("openConnections") || json.has("OpenConnections") || json.has("lineTable") || json.has("LineTable")) {
						JsonArray arr = json.has("externalEndpoints") ? json.getAsJsonArray("externalEndpoints")
								: json.has("ExternalEndpoints") ? json.getAsJsonArray("ExternalEndpoints")
								: json.has("openConnections") ? json.getAsJsonArray("openConnections")
								: json.has("OpenConnections") ? json.getAsJsonArray("OpenConnections")
								: json.has("lineTable") ? json.getAsJsonArray("lineTable") : json.getAsJsonArray("LineTable");
						List<MultiProtocolSocketAddress> list = new ArrayList<>();
						for (JsonElement el : arr) {
							if (el.isJsonPrimitive()) {
								list.add(new MultiProtocolSocketAddress(el.getAsString()));
							}
						}
						current.setExternalEndpoints(list);
					}

					if (json.has("autoConnections") || json.has("AutoConnections") || json.has("connectLineTable") || json.has("ConnectLineTable")) {
						JsonArray arr = json.has("autoConnections") ? json.getAsJsonArray("autoConnections")
								: json.has("AutoConnections") ? json.getAsJsonArray("AutoConnections")
								: json.has("connectLineTable") ? json.getAsJsonArray("connectLineTable") : json.getAsJsonArray("ConnectLineTable");
						List<MultiProtocolSocketAddress> list = new ArrayList<>();
						for (JsonElement el : arr) {
							if (el.isJsonPrimitive()) {
								list.add(new MultiProtocolSocketAddress(el.getAsString()));
							}
						}
						current.setAutoConnections(list);
					}

					if (json.has("ntpServers") || json.has("NtpServers") || json.has("ntpServerTable")) {
						JsonArray arr = json.has("ntpServers") ? json.getAsJsonArray("ntpServers")
								: json.has("NtpServers") ? json.getAsJsonArray("NtpServers")
								: json.getAsJsonArray("ntpServerTable");
						List<MultiProtocolSocketAddress> list = new ArrayList<>();
						for (JsonElement el : arr) {
							if (el.isJsonPrimitive()) {
								list.add(new MultiProtocolSocketAddress(el.getAsString()));
							}
						}
						current.setNtpServers(list);
					}

					if (json.has("dns") || json.has("DNS")) {
						JsonArray arr = json.has("dns") ? json.getAsJsonArray("dns") : json.getAsJsonArray("DNS");
						List<InetAddress> list = new ArrayList<>();
						for (JsonElement el : arr) {
							if (el.isJsonPrimitive()) {
								try {
									list.add(InetAddress.getByName(el.getAsString()));
								} catch (Exception ignored) {}
							}
						}
						current.setDNS(list);
					}

					if (json.has("extraRoutes") || json.has("ExtraRoutes")) {
						JsonArray arr = json.has("extraRoutes") ? json.getAsJsonArray("extraRoutes") : json.getAsJsonArray("ExtraRoutes");
						List<String> list = new ArrayList<>();
						for (JsonElement el : arr) {
							if (el.isJsonPrimitive()) {
								list.add(el.getAsString());
							}
						}
						current.setExtraRoutes(list);
					}

					if (json.has("networkInterfaceExcepts") || json.has("NetworkInterfaceExcepts")) {
						JsonArray arr = json.has("networkInterfaceExcepts") ? json.getAsJsonArray("networkInterfaceExcepts") : json.getAsJsonArray("NetworkInterfaceExcepts");
						List<String> list = new ArrayList<>();
						for (JsonElement el : arr) {
							if (el.isJsonPrimitive()) {
								list.add(el.getAsString());
							}
						}
						current.setNetworkInterfaceExcepts(list);
					}

					if (json.has("congestionAlgorithm") && !json.get("congestionAlgorithm").isJsonNull()) {
						current.setCongestionAlgorithm(json.get("congestionAlgorithm").getAsString());
					}

					if (json.has("performanceStrategy") && !json.get("performanceStrategy").isJsonNull()) {
						current.setPerformanceStrategy(json.get("performanceStrategy").getAsString());
					}

					if (json.has("linkConnectionsCount")) {
						current.setLinkConnectionsCount(json.get("linkConnectionsCount").getAsInt());
					}

					if (json.has("burstLimit")) {
						current.setBurstLimit(json.get("burstLimit").getAsDouble());
					}

					if (json.has("delayUpperBound")) {
						current.setDelayUpperBound(json.get("delayUpperBound").getAsDouble());
					}

					if (json.has("delayLowerBound")) {
						current.setDelayLowerBound(json.get("delayLowerBound").getAsDouble());
					}

					if (json.has("nagleDelayTime")) {
						current.setNagleDelayTime(json.get("nagleDelayTime").getAsLong());
					}

					if (json.has("linkNagleDelayTime")) {
						current.setLinkNagleDelayTime(json.get("linkNagleDelayTime").getAsLong());
					}

					if (json.has("denyExternalEndpointQuery")) {
						current.setDenyExternalEndpointQuery(json.get("denyExternalEndpointQuery").getAsBoolean());
					} else if (json.has("denyConnectionQuery")) {
						current.setDenyExternalEndpointQuery(json.get("denyConnectionQuery").getAsBoolean());
					} else if (json.has("denyLineTableQuery")) {
						current.setDenyExternalEndpointQuery(json.get("denyLineTableQuery").getAsBoolean());
					}

					if (json.has("denyExternalEndpointBroadcast")) {
						current.setDenyExternalEndpointBroadcast(json.get("denyExternalEndpointBroadcast").getAsBoolean());
					} else if (json.has("denyConnectionBroadcast")) {
						current.setDenyExternalEndpointBroadcast(json.get("denyConnectionBroadcast").getAsBoolean());
					} else if (json.has("denyLineTableBroadcast")) {
						current.setDenyExternalEndpointBroadcast(json.get("denyLineTableBroadcast").getAsBoolean());
					}

					// Trigger GUI save consumer or save directly
					if (proxySystem.getKLALBGUI() != null && proxySystem.getKLALBGUI().getSaveComsumer() != null) {
						proxySystem.getKLALBGUI().getSaveComsumer().accept(proxySystem.getConfig());
					} else {
						proxySystem.saveConfigToFile();
					}

					JsonObject resp = new JsonObject();
					resp.addProperty("success", true);
					resp.addProperty("message", "Configuration updated successfully");
					sendJsonResponse(exchange, 200, resp);
				} else {
					sendError(exchange, 500, "Current configuration is null");
				}
			} catch (Exception e) {
				sendError(exchange, 400, "Failed to update configuration: " + e.getMessage());
			}
		} else {
			sendError(exchange, 405, "Method not allowed");
		}
	}

	// -------------------------------------------------------------
	// SSE Handler & Broadcaster
	// -------------------------------------------------------------

	private void handleEvents(HttpExchange exchange) throws IOException {
		if (handleCorsPreflight(exchange)) return;
		if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
			sendError(exchange, 405, "Method not allowed");
			return;
		}

		Headers headers = exchange.getResponseHeaders();
		headers.set("Content-Type", "text/event-stream; charset=utf-8");
		headers.set("Cache-Control", "no-cache, no-transform");
		headers.set("Connection", "keep-alive");
		headers.set("Access-Control-Allow-Origin", "*");

		exchange.sendResponseHeaders(200, 0);
		sseClients.add(exchange);
	}

	private void startSseBroadcaster() {
		sseExecutor.scheduleAtFixedRate(() -> {
			if (sseClients.isEmpty()) return;

			JsonObject eventData = new JsonObject();
			eventData.add("status", getStatusJson());
			eventData.add("links", getLinksJson());
			String eventStr = "data: " + eventData.toString() + "\n\n";
			byte[] bytes = eventStr.getBytes(StandardCharsets.UTF_8);

			Iterator<HttpExchange> it = sseClients.iterator();
			while (it.hasNext()) {
				HttpExchange ex = it.next();
				try {
					OutputStream os = ex.getResponseBody();
					os.write(bytes);
					os.flush();
				} catch (Exception e) {
					try {
						ex.close();
					} catch (Exception ignored) {}
					it.remove();
				}
			}
		}, 200, 200, TimeUnit.MILLISECONDS);
	}

	// -------------------------------------------------------------
	// Static Resource / SPA Handler
	// -------------------------------------------------------------

	private void handleStatic(HttpExchange exchange) throws IOException {
		if (handleCorsPreflight(exchange)) return;

		String path = exchange.getRequestURI().getPath();
		if (path.startsWith("/api/")) {
			sendError(exchange, 404, "API endpoint not found");
			return;
		}

		// Look for dashboard/dist
		File distDir = new File("dashboard/dist");
		File targetFile = null;

		if (distDir.exists() && distDir.isDirectory()) {
			String relPath = path.equals("/") ? "index.html" : path.substring(1);
			targetFile = new File(distDir, relPath);
			if (!targetFile.exists() || targetFile.isDirectory()) {
				targetFile = new File(distDir, "index.html");
			}
		}

		if (targetFile != null && targetFile.exists() && targetFile.isFile()) {
			String mime = getMimeType(targetFile.getName());
			byte[] content = Files.readAllBytes(targetFile.toPath());
			setCorsHeaders(exchange);
			exchange.getResponseHeaders().set("Content-Type", mime);
			exchange.sendResponseHeaders(200, content.length);
			try (OutputStream os = exchange.getResponseBody()) {
				os.write(content);
			}
		} else {
			// Fallback welcome message if frontend not built yet
			String html = "<!DOCTYPE html><html><head><meta charset='utf-8'><title>KLALB Web Dashboard</title>" +
					"<style>body{font-family:system-ui,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0;background:#09090b;color:#fafafa}" +
					".card{background:#18181b;padding:2rem;border-radius:0.75rem;border:1px solid #27272a;max-width:480px;text-align:center}" +
					"h1{margin:0 0 0.5rem;font-size:1.5rem}p{color:#a1a1aa;margin:0 0 1rem;font-size:0.875rem}code{background:#27272a;padding:0.2rem 0.4rem;border-radius:0.25rem}</style></head>" +
					"<body><div class='card'><h1>KLALB Web Dashboard API Active</h1>" +
					"<p>The backend API is ready. Start the frontend dev server or build the dashboard: <br><code>cd dashboard && pnpm build</code></p>" +
					"<p><a href='/api/status' style='color:#38bdf8'>View /api/status</a></p></div></body></html>";
			byte[] bytes = html.getBytes(StandardCharsets.UTF_8);
			setCorsHeaders(exchange);
			exchange.getResponseHeaders().set("Content-Type", "text/html; charset=utf-8");
			exchange.sendResponseHeaders(200, bytes.length);
			try (OutputStream os = exchange.getResponseBody()) {
				os.write(bytes);
			}
		}
	}

	private String getMimeType(String filename) {
		String lower = filename.toLowerCase();
		if (lower.endsWith(".html")) return "text/html; charset=utf-8";
		if (lower.endsWith(".js") || lower.endsWith(".mjs")) return "application/javascript; charset=utf-8";
		if (lower.endsWith(".css")) return "text/css; charset=utf-8";
		if (lower.endsWith(".json")) return "application/json; charset=utf-8";
		if (lower.endsWith(".svg")) return "image/svg+xml";
		if (lower.endsWith(".png")) return "image/png";
		if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
		if (lower.endsWith(".ico")) return "image/x-icon";
		if (lower.endsWith(".woff2")) return "font/woff2";
		if (lower.endsWith(".woff")) return "font/woff";
		if (lower.endsWith(".ttf")) return "font/ttf";
		return "application/octet-stream";
	}
}
