import {
  diffGraphSnapshots
} from "./chunk-3SLRNYNG.js";
import {
  graphSnapshotFromMemoryDir
} from "./chunk-LIRZNNUP.js";

// src/dashboard-runtime.ts
import { createHash } from "crypto";
import { createServer } from "http";
import { watch } from "fs";
import { readFile } from "fs/promises";
import path from "path";
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "::1"]);
function normalizeOriginHostname(hostname) {
  if (hostname.startsWith("[") && hostname.endsWith("]")) {
    return hostname.slice(1, -1);
  }
  return hostname;
}
function websocketAcceptKey(clientKey) {
  return createHash("sha1").update(`${clientKey}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest("base64");
}
function encodeTextFrame(payload) {
  const payloadBuffer = Buffer.from(payload, "utf-8");
  const len = payloadBuffer.length;
  const header = [129];
  if (len <= 125) {
    header.push(len);
  } else if (len <= 65535) {
    header.push(126, len >> 8 & 255, len & 255);
  } else {
    const high = Math.floor(len / 2 ** 32);
    const low = len >>> 0;
    header.push(127, high >> 24 & 255, high >> 16 & 255, high >> 8 & 255, high & 255, low >> 24 & 255, low >> 16 & 255, low >> 8 & 255, low & 255);
  }
  return Buffer.concat([Buffer.from(header), payloadBuffer]);
}
var GraphDashboardServer = class {
  memoryDir;
  host;
  requestedPort;
  publicDir;
  watchDebounceMs;
  server = null;
  watcher = null;
  clients = /* @__PURE__ */ new Map();
  graphSnapshot = {
    generatedAt: (/* @__PURE__ */ new Date(0)).toISOString(),
    nodes: [],
    edges: [],
    stats: { nodes: 0, edges: 0, malformedLines: 0, filesMissing: [] }
  };
  debounceTimer = null;
  lastError = null;
  boundPort = 0;
  constructor(options) {
    this.memoryDir = options.memoryDir;
    this.host = options.host?.trim() || "127.0.0.1";
    this.requestedPort = Number.isFinite(options.port) ? Math.max(0, Math.floor(options.port ?? 0)) : 0;
    this.publicDir = options.publicDir ?? path.join(process.cwd(), "dashboard", "public");
    this.watchDebounceMs = Math.max(50, Math.floor(options.watchDebounceMs ?? 300));
  }
  async start() {
    if (this.server) {
      return this.status();
    }
    await this.rebuildSnapshot();
    const candidate = createServer((req, res) => {
      void this.handleHttp(req, res);
    });
    candidate.on("upgrade", (req, socket) => {
      this.handleUpgrade(req, socket);
    });
    try {
      await new Promise((resolve, reject) => {
        const onError = (err) => {
          candidate.off("listening", onListening);
          reject(err);
        };
        const onListening = () => {
          candidate.off("error", onError);
          resolve();
        };
        candidate.once("error", onError);
        candidate.once("listening", onListening);
        candidate.listen(this.requestedPort, this.host);
      });
    } catch (err) {
      this.lastError = err instanceof Error ? err.message : String(err);
      try {
        candidate.close();
      } catch {
      }
      throw err;
    }
    this.server = candidate;
    const addr = candidate.address();
    this.boundPort = typeof addr === "object" && addr ? addr.port : this.requestedPort;
    this.startWatcher();
    return this.status();
  }
  async stop() {
    const closeServer = this.server;
    this.server = null;
    this.boundPort = 0;
    if (this.debounceTimer) {
      clearTimeout(this.debounceTimer);
      this.debounceTimer = null;
    }
    if (this.watcher) {
      this.watcher.close();
      this.watcher = null;
    }
    for (const client of this.clients.values()) {
      try {
        client.socket.destroy();
      } catch {
      }
    }
    this.clients.clear();
    if (closeServer) {
      await new Promise((resolve, reject) => {
        closeServer.close((err) => {
          if (err) reject(err);
          else resolve();
        });
      });
    }
  }
  status() {
    return {
      running: this.server !== null,
      host: this.host,
      port: this.boundPort,
      watching: this.watcher !== null,
      lastUpdatedAt: this.graphSnapshot.generatedAt,
      graphNodeCount: this.graphSnapshot.stats.nodes,
      graphEdgeCount: this.graphSnapshot.stats.edges
    };
  }
  async handleHttp(req, res) {
    const url = req.url ?? "/";
    if (req.method === "GET" && url === "/api/health") {
      this.respondJson(res, 200, {
        ok: true,
        running: this.server !== null,
        watching: this.watcher !== null,
        graph: this.graphSnapshot.stats,
        clients: this.clients.size,
        lastError: this.lastError ?? void 0
      });
      return;
    }
    if (req.method === "GET" && url === "/api/graph") {
      this.respondJson(res, 200, this.graphSnapshot);
      return;
    }
    if (req.method === "GET" && url === "/app.js") {
      await this.respondStatic(res, path.join(this.publicDir, "app.js"), "application/javascript; charset=utf-8");
      return;
    }
    if (req.method === "GET" && (url === "/" || url === "/index.html")) {
      await this.respondStatic(res, path.join(this.publicDir, "index.html"), "text/html; charset=utf-8");
      return;
    }
    this.respondJson(res, 404, { error: "Not found" });
  }
  respondJson(res, status, payload) {
    const body = JSON.stringify(payload, null, 2);
    res.statusCode = status;
    res.setHeader("content-type", "application/json; charset=utf-8");
    res.setHeader("content-length", String(Buffer.byteLength(body)));
    res.end(body);
  }
  async respondStatic(res, filePath, contentType) {
    try {
      const body = await readFile(filePath, "utf-8");
      res.statusCode = 200;
      res.setHeader("content-type", contentType);
      res.setHeader("content-length", String(Buffer.byteLength(body)));
      res.end(body);
    } catch {
      this.respondJson(res, 404, { error: "Not found" });
    }
  }
  handleUpgrade(req, socket) {
    const upgrade = typeof req.headers.upgrade === "string" ? req.headers.upgrade.toLowerCase() : "";
    const key = req.headers["sec-websocket-key"];
    const origin = typeof req.headers.origin === "string" ? req.headers.origin : "";
    if (upgrade !== "websocket" || typeof key !== "string") {
      socket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
      socket.destroy();
      return;
    }
    if (!this.isAllowedOrigin(origin)) {
      socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
      socket.destroy();
      return;
    }
    const accept = websocketAcceptKey(key);
    socket.write(
      [
        "HTTP/1.1 101 Switching Protocols",
        "Upgrade: websocket",
        "Connection: Upgrade",
        `Sec-WebSocket-Accept: ${accept}`,
        "",
        ""
      ].join("\r\n")
    );
    const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    this.clients.set(id, { id, socket });
    socket.on("close", () => {
      this.clients.delete(id);
    });
    socket.on("error", () => {
      this.clients.delete(id);
    });
    const hello = JSON.stringify({
      type: "hello",
      graph: this.graphSnapshot
    });
    socket.write(encodeTextFrame(hello));
  }
  isAllowedOrigin(origin) {
    if (!origin) return false;
    try {
      const parsed = new URL(origin);
      const hostname = normalizeOriginHostname(parsed.hostname);
      if (!LOOPBACK_HOSTS.has(hostname)) return false;
      if (parsed.protocol !== "http:") return false;
      const originPort = parsed.port ? Number(parsed.port) : 80;
      return Number.isFinite(originPort) && originPort === this.boundPort;
    } catch {
      return false;
    }
  }
  broadcast(payload) {
    const frame = encodeTextFrame(JSON.stringify(payload));
    for (const [id, client] of this.clients.entries()) {
      try {
        client.socket.write(frame);
      } catch {
        this.clients.delete(id);
      }
    }
  }
  startWatcher() {
    const graphDir = path.join(this.memoryDir, "state", "graphs");
    try {
      this.watcher = watch(graphDir, { persistent: false }, () => {
        this.scheduleRebuild();
      });
      this.watcher.on("error", (err) => {
        this.lastError = err instanceof Error ? err.message : String(err);
      });
    } catch (err) {
      this.lastError = err instanceof Error ? err.message : String(err);
      this.watcher = null;
    }
  }
  scheduleRebuild() {
    if (this.debounceTimer) clearTimeout(this.debounceTimer);
    this.debounceTimer = setTimeout(() => {
      this.debounceTimer = null;
      void this.rebuildAndBroadcast();
    }, this.watchDebounceMs);
  }
  async rebuildAndBroadcast() {
    const previous = this.graphSnapshot;
    await this.rebuildSnapshot();
    const patch = diffGraphSnapshots(previous, this.graphSnapshot);
    if (patch.addedEdges.length === 0 && patch.removedEdges.length === 0 && patch.addedNodes.length === 0 && patch.removedNodes.length === 0) {
      return;
    }
    this.broadcast({
      type: "graph_patch",
      generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
      patch,
      graph: this.graphSnapshot
    });
  }
  async rebuildSnapshot() {
    try {
      this.graphSnapshot = await graphSnapshotFromMemoryDir(this.memoryDir);
      this.lastError = null;
    } catch (err) {
      this.lastError = err instanceof Error ? err.message : String(err);
    }
  }
};

export {
  GraphDashboardServer
};
//# sourceMappingURL=chunk-ZPKBYX2F.js.map