import { createServer } from "node:http";
import { readFileSync, existsSync, statSync } from "node:fs";
import { resolve, join, extname } from "node:path";
import { fileURLToPath } from "node:url";
import { createGzip } from "node:zlib";
import { pipeline } from "node:stream";
import { Config } from "./core/config.js";
import { Store } from "./core/store.js";
import { MapxGraph } from "./core/graph.js";
import { calculateMetrics, calculateGraphMetrics, calculateDSM } from "./core/metrics.js";
import { ContextBuilder } from "./core/context-builder.js";
import { RouteRegistry } from "./frameworks/route-registry.js";
import { UiEventBus, getToolCallsLogPath } from "./ui-events.js";
import { getChangedFiles, isGitRepo } from "./core/git-tracker.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = resolve(__filename, "..");
function findUiDir() {
  if (existsSync(__filename)) {
    const root = resolve(__dirname, "..");
    if (existsSync(join(root, "dist/ui/main.js"))) {
      return join(root, "dist/ui");
    }
    if (existsSync(join(root, "ui/main.js"))) {
      return join(root, "ui");
    }
  }
  const binDir = resolve(process.execPath, "..");
  const candidates = [
    join(binDir, "ui"),
    // next to binary
    resolve(binDir, "..", "share", "mapx", "ui"),
    // XDG system
    join(process.env["HOME"] ?? "", ".local", "share", "mapx", "ui")
    // XDG user
  ];
  for (const dir of candidates) {
    if (existsSync(join(dir, "main.js"))) {
      return dir;
    }
  }
  return join(__dirname, "ui");
}
function startUiServer(opts) {
  const { port, host, token, dir } = opts;
  const uiDir = findUiDir();
  const rateLimitMap = /* @__PURE__ */ new Map();
  const isRateLimited = (ip) => {
    const now = Date.now();
    const windowStart = now - 6e4;
    let timestamps = rateLimitMap.get(ip) || [];
    timestamps = timestamps.filter((t) => t > windowStart);
    if (timestamps.length >= 10) {
      return true;
    }
    timestamps.push(now);
    rateLimitMap.set(ip, timestamps);
    return false;
  };
  const getClientIp = (req) => {
    return req.headers["x-forwarded-for"] || req.socket.remoteAddress || "unknown";
  };
  const checkAuth = (req) => {
    if (!token) return true;
    const auth = req.headers["authorization"] || req.headers["Authorization"];
    if (typeof auth !== "string") return false;
    return auth.trim() === `Bearer ${token}`;
  };
  const setCorsHeaders = (req, res) => {
    const origin = req.headers.origin;
    if (origin && /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin)) {
      res.setHeader("Access-Control-Allow-Origin", origin);
    } else {
      res.setHeader("Access-Control-Allow-Origin", "*");
    }
    res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
    res.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
  };
  const server = createServer(async (req, res) => {
    setCorsHeaders(req, res);
    if (req.method === "OPTIONS") {
      res.writeHead(204);
      res.end();
      return;
    }
    const ip = getClientIp(req);
    const parsedUrl = new URL(req.url || "/", `http://${host}:${port}`);
    const pathname = parsedUrl.pathname;
    if (pathname.startsWith("/api") || pathname === "/events") {
      if (!checkAuth(req)) {
        res.writeHead(401, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ error: "Unauthorized: invalid or missing Bearer token" }));
        return;
      }
    }
    if (pathname === "/api/context" || pathname === "/api/graph" || pathname === "/api/graph/meta") {
      if (isRateLimited(ip)) {
        res.writeHead(429, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ error: "Rate limit exceeded: max 10 requests per minute" }));
        return;
      }
    }
    try {
      if (pathname === "/api/status") {
        const configPath = resolve(dir, ".mapx", "config.json");
        if (!existsSync(configPath)) {
          res.writeHead(400, { "Content-Type": "application/json" });
          res.end(JSON.stringify({ error: "Mapx not initialized" }));
          return;
        }
        const dbPath = resolve(dir, ".mapx", "mapx.db");
        const store = new Store(dbPath);
        try {
          const fileCount = store.getFileCount();
          const symbolCount = store.getSymbolCount();
          const edgeCount = store.getEdgeCount();
          const lastScan = store.getMeta("last_scan_time") || "never";
          const config = await Config.load(dir);
          res.writeHead(200, { "Content-Type": "application/json" });
          res.end(JSON.stringify({
            repoName: config.repo.name,
            lastScan,
            fileCount,
            symbolCount,
            edgeCount,
            languages: store.getLanguageBreakdown()
          }));
        } finally {
          try {
            store.close();
          } catch {
          }
        }
        return;
      }
      if (pathname === "/api/graph/meta") {
        const dbPath = resolve(dir, ".mapx", "mapx.db");
        const store = new Store(dbPath);
        try {
          const totalFiles = store.getFileCount();
          const totalEdges = store.getEdgeCount();
          res.writeHead(200, { "Content-Type": "application/json" });
          res.end(JSON.stringify({ totalFiles, totalEdges, totalElements: totalFiles + totalEdges }));
        } finally {
          try {
            store.close();
          } catch {
          }
        }
        return;
      }
      if (pathname === "/api/graph") {
        const dbPath = resolve(dir, ".mapx", "mapx.db");
        const store = new Store(dbPath);
        try {
          const config = await Config.load(dir);
          const graph = new MapxGraph(config.repo.name);
          const files = store.getAllFiles();
          const allSymbols = store.getAllSymbols();
          const edges = store.getAllEdges();
          for (const file of files) {
            graph.addFileNode(file.path, file.language, file.size_bytes, file.lines);
          }
          for (const sym of allSymbols) {
            graph.addSymbolNode(sym.name, sym.file_path, sym.name, sym.kind, sym.start_line, sym.end_line, sym.scope);
          }
          for (const edge of edges) {
            graph.addDependencyEdge({
              sourceFile: edge.source_file,
              targetFile: edge.target_file,
              sourceSymbol: edge.source_symbol,
              targetSymbol: edge.target_symbol,
              edgeType: edge.edge_type,
              repo: edge.repo,
              weight: edge.weight,
              verifiability: edge.verifiability,
              targetRepo: edge.target_repo
            });
          }
          const pageRankScores = graph.computePageRank();
          const symbolCountRows = store.raw.prepare(
            "SELECT file_path, COUNT(*) as cnt FROM symbols GROUP BY file_path"
          ).all();
          const symbolCountMap = /* @__PURE__ */ new Map();
          for (const row of symbolCountRows) {
            symbolCountMap.set(row.file_path, row.cnt);
          }
          const elements = [];
          for (const f of files) {
            const fPath = f.path;
            elements.push({
              data: {
                id: fPath,
                label: fPath.split("/").pop() || fPath,
                type: "file",
                language: f.language,
                size: f.size_bytes,
                lines: f.lines,
                symbolCount: symbolCountMap.get(fPath) || 0,
                pagerank: pageRankScores.get(fPath) || 0,
                role: f.role,
                roleConfidence: f.role_confidence
              }
            });
          }
          const edgeMap = /* @__PURE__ */ new Map();
          for (const e of edges) {
            const key = `${e.source_file}->${e.target_file}`;
            const existing = edgeMap.get(key);
            if (existing) {
              existing.weight = Math.max(existing.weight, e.weight);
              existing.count++;
            } else {
              edgeMap.set(key, {
                source: e.source_file,
                target: e.target_file,
                weight: e.weight,
                count: 1,
                type: e.edge_type,
                verifiability: e.verifiability
              });
            }
          }
          for (const e of edgeMap.values()) {
            elements.push({
              data: {
                id: `edge-${e.source}-${e.target}`,
                source: e.source,
                target: e.target,
                type: e.type,
                verifiability: e.verifiability,
                weight: e.weight,
                count: e.count
              }
            });
          }
          const limitParam = parsedUrl.searchParams.get("limit");
          const offsetParam = parsedUrl.searchParams.get("offset");
          const usePagination = limitParam !== null || offsetParam !== null;
          let responseBody;
          if (usePagination) {
            const total = elements.length;
            const limit = Math.max(1, parseInt(limitParam || String(total), 10));
            const offset = Math.max(0, parseInt(offsetParam || "0", 10));
            const paginatedElements = elements.slice(offset, offset + limit);
            responseBody = { elements: paginatedElements, meta: { total, limit, offset } };
          } else {
            responseBody = elements;
          }
          const payload = JSON.stringify(responseBody);
          const acceptEncoding = req.headers["accept-encoding"] || "";
          const clientAcceptsGzip = typeof acceptEncoding === "string" && acceptEncoding.includes("gzip");
          if (payload.length > 1 * 1024 * 1024 && clientAcceptsGzip) {
            res.writeHead(200, {
              "Content-Type": "application/json",
              "Content-Encoding": "gzip",
              "Transfer-Encoding": "chunked"
            });
            const { Readable } = require("node:stream");
            const source = Readable.from([payload]);
            const gzip = createGzip();
            pipeline(source, gzip, res, (err) => {
              if (err && !res.destroyed) {
                res.end();
              }
            });
          } else {
            res.writeHead(200, { "Content-Type": "application/json" });
            res.end(payload);
          }
        } finally {
          try {
            store.close();
          } catch {
          }
        }
        return;
      }
      if (pathname === "/api/clusters") {
        const dbPath = resolve(dir, ".mapx", "mapx.db");
        const store = new Store(dbPath);
        try {
          const config = await Config.load(dir);
          const repo = config.repo.name;
          const clusters = store.getClusters(repo);
          const memberships = store.getClusterMemberships(repo);
          res.writeHead(200, { "Content-Type": "application/json" });
          res.end(JSON.stringify({ clusters, memberships }));
        } finally {
          try {
            store.close();
          } catch {
          }
        }
        return;
      }
      if (pathname === "/api/profile") {
        const dbPath = resolve(dir, ".mapx", "mapx.db");
        const store = new Store(dbPath);
        try {
          const config = await Config.load(dir);
          const repo = config.repo.name;
          const profileStr = store.getMeta("codebase_profile:" + repo);
          const profile = profileStr ? JSON.parse(profileStr) : null;
          res.writeHead(200, { "Content-Type": "application/json" });
          res.end(JSON.stringify({ profile }));
        } finally {
          try {
            store.close();
          } catch {
          }
        }
        return;
      }
      if (pathname === "/api/smells") {
        const dbPath = resolve(dir, ".mapx", "mapx.db");
        const store = new Store(dbPath);
        try {
          const config = await Config.load(dir);
          const repo = config.repo.name;
          const smells = store.getArchSmells(repo);
          res.writeHead(200, { "Content-Type": "application/json" });
          res.end(JSON.stringify({ smells }));
        } finally {
          try {
            store.close();
          } catch {
          }
        }
        return;
      }
      if (pathname === "/api/dsm") {
        const dbPath = resolve(dir, ".mapx", "mapx.db");
        const store = new Store(dbPath);
        try {
          const config = await Config.load(dir);
          const repo = config.repo.name;
          const dsm = calculateDSM(store, repo);
          res.writeHead(200, { "Content-Type": "application/json" });
          res.end(JSON.stringify(dsm));
        } finally {
          try {
            store.close();
          } catch {
          }
        }
        return;
      }
      if (pathname === "/api/explain") {
        const dbPath = resolve(dir, ".mapx", "mapx.db");
        const store = new Store(dbPath);
        try {
          const file = parsedUrl.searchParams.get("file") || "";
          const signals = store.getClassificationSignals(file);
          res.writeHead(200, { "Content-Type": "application/json" });
          res.end(JSON.stringify({ signals }));
        } finally {
          try {
            store.close();
          } catch {
          }
        }
        return;
      }
      if (pathname === "/api/symbols") {
        const dbPath = resolve(dir, ".mapx", "mapx.db");
        const store = new Store(dbPath);
        try {
          const term = parsedUrl.searchParams.get("q") || "";
          const limit = Math.min(Math.max(1, parseInt(parsedUrl.searchParams.get("limit") || "50", 10)), 200);
          const offset = Math.max(0, parseInt(parsedUrl.searchParams.get("offset") || "0", 10));
          const results = store.searchSymbolsFiltered({ term, limit, offset });
          const total = store.countSymbolsFiltered({ term });
          res.writeHead(200, { "Content-Type": "application/json" });
          res.end(JSON.stringify({ results, total, limit, offset }));
        } finally {
          try {
            store.close();
          } catch {
          }
        }
        return;
      }
      if (pathname.startsWith("/api/symbol/")) {
        const dbPath = resolve(dir, ".mapx", "mapx.db");
        const store = new Store(dbPath);
        try {
          const symName = decodeURIComponent(pathname.substring("/api/symbol/".length));
          const sym = store.getSymbolByName(symName);
          if (!sym) {
            res.writeHead(404, { "Content-Type": "application/json" });
            res.end(JSON.stringify({ error: "Symbol not found" }));
            return;
          }
          const callers = store.getCallersOfSymbol(symName);
          const callees = store.getCalleesOfSymbol(symName);
          let sourceCode = "";
          if (sym.file_path) {
            try {
              const fullFilePath = resolve(dir, sym.file_path);
              if (existsSync(fullFilePath)) {
                const fileContent = readFileSync(fullFilePath, "utf-8");
                const lines = fileContent.split("\n");
                const start = Math.max(0, (sym.start_line || 1) - 1);
                const end = Math.min(lines.length, sym.end_line || lines.length);
                sourceCode = lines.slice(start, end).join("\n");
              }
            } catch {
            }
          }
          res.writeHead(200, { "Content-Type": "application/json" });
          res.end(JSON.stringify({
            symbol: sym,
            callers,
            callees,
            sourceCode
          }));
        } finally {
          try {
            store.close();
          } catch {
          }
        }
        return;
      }
      if (pathname === "/api/metrics") {
        const dbPath = resolve(dir, ".mapx", "mapx.db");
        const store = new Store(dbPath);
        try {
          const config = await Config.load(dir);
          const graph = new MapxGraph(config.repo.name);
          for (const file of store.getAllFiles()) {
            graph.addFileNode(file.path, file.language, file.size_bytes, file.lines);
          }
          for (const sym of store.getAllSymbols()) {
            graph.addSymbolNode(sym.name, sym.file_path, sym.name, sym.kind, sym.start_line, sym.end_line, sym.scope);
          }
          for (const edge of store.getAllEdges()) {
            graph.addDependencyEdge({
              sourceFile: edge.source_file,
              targetFile: edge.target_file,
              sourceSymbol: edge.source_symbol,
              targetSymbol: edge.target_symbol,
              edgeType: edge.edge_type,
              repo: edge.repo,
              weight: edge.weight,
              verifiability: edge.verifiability,
              targetRepo: edge.target_repo
            });
          }
          const fileCount = store.getFileCount();
          const symbolCount = store.getSymbolCount();
          const edgeCount = store.getEdgeCount();
          const fileMetrics = calculateMetrics(store, { repo: config.repo.name });
          const graphMetrics = calculateGraphMetrics(store, config.repo.name);
          const topFiles = store.getTopFilesByPageRank(graph, 10);
          const topSymbols = store.getTopSymbolsByPageRank(graph, 10);
          const verifiedEdges = store.raw.prepare("SELECT COUNT(*) as cnt FROM edges WHERE verifiability = 'verified'").get()?.cnt || 0;
          const inferredEdges = store.raw.prepare("SELECT COUNT(*) as cnt FROM edges WHERE verifiability = 'inferred'").get()?.cnt || 0;
          const languages = store.getLanguageBreakdown();
          const symbolKinds = store.raw.prepare(
            "SELECT kind, COUNT(*) as cnt FROM symbols GROUP BY kind ORDER BY cnt DESC"
          ).all();
          const edgeTypes = store.raw.prepare(
            "SELECT edge_type, COUNT(*) as cnt FROM edges GROUP BY edge_type ORDER BY cnt DESC"
          ).all();
          let dbSize = 0;
          try {
            dbSize = statSync(dbPath).size;
          } catch {
          }
          const repoRoot = resolve(dir, config.repo.path);
          const isGit = isGitRepo(repoRoot);
          const lastCommit = store.getMeta("last_scan_commit:" + config.repo.name) || store.getMeta("last_scan_commit");
          const gitChanges = isGit ? getChangedFiles(repoRoot, lastCommit || void 0) : [];
          res.writeHead(200, { "Content-Type": "application/json" });
          res.end(JSON.stringify({
            totalFiles: fileCount,
            totalSymbols: symbolCount,
            totalEdges: edgeCount,
            verifiedEdges,
            inferredEdges,
            languages,
            symbolKinds,
            edgeTypes,
            avgEdgesPerFile: fileCount > 0 ? (edgeCount / fileCount).toFixed(2) : "0",
            dbSize,
            density: `${(graphMetrics.density * 100).toFixed(4)}%`,
            transitivity: graphMetrics.transitivity.toFixed(4),
            git: {
              isGit,
              changesCount: gitChanges.length,
              changes: gitChanges
            },
            fileMetrics,
            topFiles,
            topSymbols
          }));
        } finally {
          try {
            store.close();
          } catch {
          }
        }
        return;
      }
      if (pathname === "/api/context") {
        if (req.method !== "POST") {
          res.writeHead(405, { "Content-Type": "application/json" });
          res.end(JSON.stringify({ error: "Method not allowed" }));
          return;
        }
        let body = "";
        req.on("data", (chunk) => {
          body += chunk;
        });
        req.on("end", async () => {
          let store = null;
          try {
            const data = JSON.parse(body);
            const task = data.task || "";
            const dbPath = resolve(dir, ".mapx", "mapx.db");
            store = new Store(dbPath);
            const config = await Config.load(dir);
            const graph = new MapxGraph(config.repo.name);
            for (const file of store.getAllFiles()) {
              graph.addFileNode(file.path, file.language, file.size_bytes, file.lines);
            }
            for (const sym of store.getAllSymbols()) {
              graph.addSymbolNode(sym.name, sym.file_path, sym.name, sym.kind, sym.start_line, sym.end_line, sym.scope);
            }
            for (const edge of store.getAllEdges()) {
              graph.addDependencyEdge({
                sourceFile: edge.source_file,
                targetFile: edge.target_file,
                sourceSymbol: edge.source_symbol,
                targetSymbol: edge.target_symbol,
                edgeType: edge.edge_type,
                repo: edge.repo,
                weight: edge.weight,
                verifiability: edge.verifiability,
                targetRepo: edge.target_repo
              });
            }
            const builder = new ContextBuilder(store, graph);
            const context = await builder.buildContext({ task, tokens: 15e3 });
            res.writeHead(200, { "Content-Type": "application/json" });
            res.end(JSON.stringify(context));
          } catch (err) {
            if (!res.headersSent) {
              res.writeHead(500, { "Content-Type": "application/json" });
              res.end(JSON.stringify({ error: err.message }));
            }
          } finally {
            if (store) {
              try {
                store.close();
              } catch {
              }
            }
          }
        });
        return;
      }
      if (pathname === "/api/routes") {
        const routeRegistry = new RouteRegistry();
        await routeRegistry.load(dir);
        res.writeHead(200, { "Content-Type": "application/json" });
        res.end(JSON.stringify({
          routes: routeRegistry.getRoutes(),
          hooks: routeRegistry.getHooks()
        }));
        return;
      }
      if (pathname === "/api/tool-calls") {
        const mapxDir = resolve(dir, ".mapx");
        const logPath = getToolCallsLogPath(mapxDir);
        const qp = new URL(req.url, "http://localhost").searchParams;
        const limit = Math.min(Math.max(1, parseInt(qp.get("limit") ?? "50", 10)), 200);
        const offset = Math.max(0, parseInt(qp.get("offset") ?? "0", 10));
        const events = [];
        let total = 0;
        if (existsSync(logPath)) {
          try {
            const content = readFileSync(logPath, "utf-8");
            const lines = content.split("\n").filter(Boolean);
            total = lines.length;
            const end = Math.max(0, total - offset);
            const start = Math.max(0, end - limit);
            for (const line of lines.slice(start, end).reverse()) {
              try {
                events.push(JSON.parse(line));
              } catch {
              }
            }
          } catch {
          }
        }
        res.writeHead(200, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ events, total }));
        return;
      }
      if (pathname === "/events") {
        res.writeHead(200, {
          "Content-Type": "text/event-stream",
          "Cache-Control": "no-cache",
          "Connection": "keep-alive"
        });
        const eventBus = UiEventBus.getInstance();
        const sendEvent = (name, data) => {
          res.write(`event: ${name}
data: ${JSON.stringify(data)}

`);
        };
        const onToolCall = (event) => sendEvent("tool-call", event);
        const onScanProgress = (event) => sendEvent("scan-progress", event);
        const onScanComplete = (event) => sendEvent("scan-complete", event);
        eventBus.on("tool-call", onToolCall);
        eventBus.on("scan-progress", onScanProgress);
        eventBus.on("scan-complete", onScanComplete);
        const mapxDir = resolve(dir, ".mapx");
        const logPath = getToolCallsLogPath(mapxDir);
        let lastSize = 0;
        try {
          lastSize = existsSync(logPath) ? statSync(logPath).size : 0;
        } catch {
        }
        const pollLogFile = () => {
          try {
            if (!existsSync(logPath)) return;
            const currentSize = statSync(logPath).size;
            if (currentSize <= lastSize) {
              if (currentSize < lastSize) lastSize = currentSize;
              return;
            }
            const { openSync, readSync, closeSync } = require("node:fs");
            const fd = openSync(logPath, "r");
            const buf = Buffer.alloc(currentSize - lastSize);
            readSync(fd, buf, 0, buf.length, lastSize);
            closeSync(fd);
            lastSize = currentSize;
            const newLines = buf.toString("utf-8").split("\n").filter(Boolean);
            for (const line of newLines) {
              try {
                const event = JSON.parse(line);
                sendEvent("tool-call", event);
              } catch {
              }
            }
          } catch {
          }
        };
        let logWatcher = null;
        try {
          if (existsSync(logPath)) {
            const { watch } = require("node:fs");
            logWatcher = watch(logPath, (eventType) => {
              if (eventType === "change") {
                pollLogFile();
              }
            });
          }
        } catch {
        }
        const pollInterval = setInterval(pollLogFile, 500);
        req.on("close", () => {
          eventBus.off("tool-call", onToolCall);
          eventBus.off("scan-progress", onScanProgress);
          eventBus.off("scan-complete", onScanComplete);
          if (logWatcher) {
            try {
              logWatcher.close();
            } catch {
            }
          }
          clearInterval(pollInterval);
        });
        return;
      }
      const rawAssetPath = pathname === "/" ? "/index.html" : pathname;
      const safeAssetPath = join(uiDir, rawAssetPath);
      if (!safeAssetPath.startsWith(uiDir)) {
        res.writeHead(403, { "Content-Type": "text/plain" });
        res.end("Forbidden: Path traversal rejected");
        return;
      }
      if (existsSync(safeAssetPath)) {
        const ext = extname(safeAssetPath);
        const contentTypes = {
          ".html": "text/html",
          ".js": "application/javascript",
          ".css": "text/css",
          ".svg": "image/svg+xml",
          ".json": "application/json",
          ".png": "image/png"
        };
        const contentType = contentTypes[ext] || "application/octet-stream";
        res.writeHead(200, { "Content-Type": contentType });
        res.end(readFileSync(safeAssetPath));
      } else {
        res.writeHead(404, { "Content-Type": "text/plain" });
        res.end("Not Found");
      }
    } catch (err) {
      if (!res.headersSent) {
        res.writeHead(500, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ error: err.message }));
      }
    }
  });
  server.listen(port, host, () => {
    console.log(`Mapx UI Server running at http://${host}:${port}`);
  });
  return server;
}
export {
  startUiServer
};
