import fs from "fs";
import path from "path";
import { TESTKIT_COVERAGE_GRAPH_VERSION } from "@elench/testkit-protocol";
import {
  DEFAULT_DISCOVERY_EXCLUDES,
  normalizeDiscoveryConfig,
  normalizeDiscoveryPath,
  shouldExcludeDiscoveryPath,
} from "../discovery/path-policy.mjs";

const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE"];
const HTTP_WRAPPER_METHODS = {
  getJson: "GET",
  postJson: "POST",
  putJson: "PUT",
  patchJson: "PATCH",
  deleteJson: "DELETE",
};

export function buildCoverageGraph({ productDir, repoDiscovery = {}, services = {}, discoveryFiles = [] }) {
  const graph = createEmptyGraph();
  const serviceContexts = new Map();
  const normalizedRepoDiscovery = normalizeDiscoveryConfig(repoDiscovery, { allowRoots: true });

  for (const [serviceName, config] of Object.entries(services)) {
    const context = buildServiceCoverageContext(productDir, serviceName, config, normalizedRepoDiscovery);
    if (!context) continue;
    serviceContexts.set(serviceName, context);
    appendGraph(graph, context.graph);
  }

  const evidence = [];
  for (const entry of discoveryFiles) {
    const context = serviceContexts.get(entry.serviceName);
    const testNodeId = createTestFileNode(graph, entry);
    if (!context) {
      evidence.push({
        id: `evidence:${entry.serviceName}:${entry.filePath}`,
        source: "convention",
        confidence: "medium",
        service: entry.serviceName,
        suiteName: entry.suiteName,
        selectionType: toSelectionType(entry.type, entry.framework),
        framework: entry.framework,
        testFilePath: entry.filePath,
        coveredNodeIds: [testNodeId],
      });
      continue;
    }

    const coveredNodeIds = inferCoveredNodeIdsForTest(entry, context);
    if (coveredNodeIds.length === 0) {
      evidence.push({
        id: `evidence:${entry.serviceName}:${entry.filePath}`,
        source: "convention",
        confidence: "medium",
        service: entry.serviceName,
        suiteName: entry.suiteName,
        selectionType: toSelectionType(entry.type, entry.framework),
        framework: entry.framework,
        testFilePath: entry.filePath,
        coveredNodeIds: [testNodeId],
      });
      continue;
    }

    for (const nodeId of coveredNodeIds) {
      graph.edges.push({
        id: `covers:${testNodeId}:${nodeId}`,
        kind: "covers",
        from: testNodeId,
        to: nodeId,
        confidence: "high",
      });
    }

    evidence.push({
      id: `evidence:${entry.serviceName}:${entry.filePath}`,
      source: "convention",
      confidence: "high",
      service: entry.serviceName,
      suiteName: entry.suiteName,
      selectionType: toSelectionType(entry.type, entry.framework),
      framework: entry.framework,
      testFilePath: entry.filePath,
      coveredNodeIds,
      details: buildEvidenceDetails(coveredNodeIds, graph),
    });
  }

  graph.evidence = evidence.sort((left, right) => left.id.localeCompare(right.id));
  graph.nodes.sort((left, right) => left.id.localeCompare(right.id));
  graph.edges.sort((left, right) => left.id.localeCompare(right.id));
  return graph;
}

export function buildServiceCoverageContext(productDir, serviceName, config, repoDiscovery = {}) {
  const serviceRoot = resolveServiceRoot(productDir, config);
  const nextAppRoot = findNextAppRoot(serviceRoot);
  if (!nextAppRoot) return null;
  const serviceDiscovery = normalizeDiscoveryConfig(config?.discovery, { allowRoots: true });
  const exclude = [
    ...new Set([
      ...DEFAULT_DISCOVERY_EXCLUDES,
      ...((repoDiscovery.exclude || []).map(normalizePath)),
      ...((serviceDiscovery.exclude || []).map(normalizePath)),
    ]),
  ];

  const graph = createEmptyGraph();
  const pages = discoverPageViews(serviceName, serviceRoot, nextAppRoot, exclude);
  const apiRoutes = discoverApiRoutes(serviceName, serviceRoot, nextAppRoot, exclude);
  const serverActions = discoverServerActions(serviceName, serviceRoot, exclude);

  for (const node of [...pages.nodes, ...apiRoutes.nodes, ...serverActions.nodes]) {
    graph.nodes.push(node);
  }
  for (const edge of [...pages.edges, ...apiRoutes.edges, ...serverActions.edges]) {
    graph.edges.push(edge);
  }

  const pageByRoute = new Map(pages.pageEntries.map((entry) => [entry.route, entry]));
  const apiRouteByKey = new Map(
    apiRoutes.routeEntries.map((entry) => [apiRouteLookupKey(entry.method, entry.requestPath), entry])
  );
  const serverActionByExportKey = new Map(
    serverActions.actionEntries.map((entry) => [`${entry.sourceFile}#${entry.exportName}`, entry])
  );

  for (const pageEntry of pages.pageEntries) {
    for (const request of pageEntry.requests) {
      const apiRoute = apiRouteByKey.get(apiRouteLookupKey(request.method, request.path));
      if (!apiRoute) continue;
      graph.edges.push({
        id: `requests:${pageEntry.node.id}:${request.node.id}`,
        kind: "requests",
        from: pageEntry.node.id,
        to: request.node.id,
        confidence: "high",
      });
      graph.edges.push({
        id: `handles:${request.node.id}:${apiRoute.node.id}`,
        kind: "handles",
        from: request.node.id,
        to: apiRoute.node.id,
        confidence: "high",
      });
    }

    for (const actionRef of pageEntry.serverActionRefs) {
      const serverAction = serverActionByExportKey.get(actionRef.exportKey);
      if (!serverAction) continue;
      graph.edges.push({
        id: `triggers:${pageEntry.node.id}:${serverAction.node.id}`,
        kind: "triggers",
        from: pageEntry.node.id,
        to: serverAction.node.id,
        confidence: actionRef.confidence,
      });
    }
  }

  return {
    serviceName,
    serviceRoot,
    nextAppRoot,
    graph,
    pageByRoute,
    apiRouteByKey,
    serverActionByExportKey,
  };
}

function createEmptyGraph() {
  return {
    schemaVersion: TESTKIT_COVERAGE_GRAPH_VERSION,
    nodes: [],
    edges: [],
    evidence: [],
  };
}

function appendGraph(graph, fragment) {
  const nodeIds = new Set(graph.nodes.map((node) => node.id));
  for (const node of fragment.nodes) {
    if (nodeIds.has(node.id)) continue;
    nodeIds.add(node.id);
    graph.nodes.push(node);
  }
  const edgeIds = new Set(graph.edges.map((edge) => edge.id));
  for (const edge of fragment.edges) {
    if (edgeIds.has(edge.id)) continue;
    edgeIds.add(edge.id);
    graph.edges.push(edge);
  }
}

function createTestFileNode(graph, entry) {
  const nodeId = `test_file:${entry.serviceName}:${entry.filePath}`;
  if (!graph.nodes.some((node) => node.id === nodeId)) {
    graph.nodes.push({
      id: nodeId,
      kind: "test_file",
      service: entry.serviceName,
      label: path.basename(entry.filePath),
      filePath: entry.filePath,
      metadata: {
        suiteName: entry.suiteName,
        framework: entry.framework,
        type: toSelectionType(entry.type, entry.framework),
      },
    });
  }
  return nodeId;
}

function inferCoveredNodeIdsForTest(entry, context) {
  const coveredNodeIds = new Set();
  const selectionType = toSelectionType(entry.type, entry.framework);

  if (entry.framework === "playwright") {
    const route = inferPageRouteFromTestFile(entry.filePath, context.nextAppRoot, context.serviceRoot);
    if (route && context.pageByRoute.has(route)) {
      coveredNodeIds.add(context.pageByRoute.get(route).node.id);
    }
  }

  if (selectionType === "int") {
    const apiRoutes = inferApiRoutesFromTestFile(entry.filePath, context.nextAppRoot, context.serviceRoot);
    for (const route of apiRoutes) {
      for (const method of HTTP_METHODS) {
        const routeEntry = context.apiRouteByKey.get(apiRouteLookupKey(method, toApiRequestPath(route)));
        if (routeEntry) coveredNodeIds.add(routeEntry.node.id);
      }
    }
  }

  return [...coveredNodeIds].sort();
}

function buildEvidenceDetails(coveredNodeIds, graph) {
  const routes = new Set();
  const requestPaths = new Set();
  const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
  for (const nodeId of coveredNodeIds) {
    const node = nodeById.get(nodeId);
    if (!node) continue;
    if (node.route) routes.add(node.route);
    if (node.path) requestPaths.add(node.path);
  }
  const details = {};
  if (routes.size === 1) {
    details.route = [...routes][0];
  }
  if (requestPaths.size > 0) {
    details.requestPaths = [...requestPaths].sort();
  }
  return Object.keys(details).length > 0 ? details : undefined;
}

function discoverPageViews(serviceName, serviceRoot, nextAppRoot, exclude = []) {
  const pageFiles = walkFiles(nextAppRoot, { baseDir: serviceRoot, exclude }).filter(
    (filePath) => filePath.endsWith("/page.tsx") || filePath.endsWith("/page.ts")
  );
  const nodes = [];
  const edges = [];
  const pageEntries = [];

  for (const filePath of pageFiles) {
    const absolutePath = filePath;
    const relativePath = normalizePath(path.relative(serviceRoot, absolutePath));
    const route = routeFromAppFile(nextAppRoot, absolutePath);
    const content = fs.readFileSync(absolutePath, "utf8");
    const node = {
      id: `page_view:${serviceName}:${route}`,
      kind: "page_view",
      service: serviceName,
      label: pageLabelFromRoute(route),
      route,
      filePath: relativePath,
    };
    nodes.push(node);

    const requests = extractClientRequests(serviceName, relativePath, content).map((request) => {
      nodes.push(request.node);
      return request;
    });

    const serverActionRefs = extractPageServerActionRefs(serviceRoot, relativePath, content);
    pageEntries.push({
      node,
      route,
      filePath: relativePath,
      requests,
      serverActionRefs,
    });
  }

  return { nodes, edges, pageEntries };
}

function discoverApiRoutes(serviceName, serviceRoot, nextAppRoot, exclude = []) {
  const routeFiles = walkFiles(path.join(nextAppRoot, "api"), { baseDir: serviceRoot, exclude }).filter(
    (filePath) => filePath.endsWith("/route.ts") || filePath.endsWith("/route.tsx") || filePath.endsWith("/route.js")
  );
  const nodes = [];
  const edges = [];
  const routeEntries = [];

  for (const absoluteOrRelative of routeFiles) {
    const filePath = normalizePath(path.relative(serviceRoot, absoluteOrRelative));
    const route = routeFromApiFile(nextAppRoot, absoluteOrRelative);
    const requestPath = toApiRequestPath(route);
    const content = fs.readFileSync(path.join(serviceRoot, filePath), "utf8");
    const methodBodies = extractExportedMethodBodies(content);
    const backendImports = extractBackendImports(serviceName, serviceRoot, filePath, content);

    for (const [method, body] of methodBodies) {
      const node = {
        id: `api_route:${serviceName}:${method}:${requestPath}`,
        kind: "api_route",
        service: serviceName,
        label: `${method} ${requestPath}`,
        route,
        method,
        path: requestPath,
        filePath,
      };
      nodes.push(node);

      const matchedCapabilities = backendImports.filter((entry) => hasWord(body, entry.importName));
      for (const capability of matchedCapabilities) {
        nodes.push(capability.node);
        edges.push({
          id: `delegates_to:${node.id}:${capability.node.id}`,
          kind: "delegates_to",
          from: node.id,
          to: capability.node.id,
          confidence: "high",
        });
      }

      routeEntries.push({ node, method, route, requestPath, filePath });
    }
  }

  return { nodes: dedupeNodes(nodes), edges, routeEntries };
}

function discoverServerActions(serviceName, serviceRoot, exclude = []) {
  const appRoots = [path.join(serviceRoot, "app"), path.join(serviceRoot, "src", "app")].filter((candidate) =>
    fs.existsSync(candidate)
  );
  const nodes = [];
  const edges = [];
  const actionEntries = [];

  for (const appRoot of appRoots) {
    for (const absolutePath of walkFiles(appRoot, { baseDir: serviceRoot, exclude })) {
      if (!absolutePath.endsWith(".ts") && !absolutePath.endsWith(".tsx")) continue;
      const content = fs.readFileSync(absolutePath, "utf8");
      if (!isServerActionFile(content)) continue;

      const relativePath = normalizePath(path.relative(serviceRoot, absolutePath));
      const backendImports = extractBackendImports(serviceName, serviceRoot, relativePath, content);
      const exportedFunctions = extractExportedFunctions(content);

      for (const exported of exportedFunctions) {
        const node = {
          id: `server_action:${serviceName}:${relativePath}#${exported.name}`,
          kind: "server_action",
          service: serviceName,
          label: exported.name,
          filePath: relativePath,
        };
        nodes.push(node);
        actionEntries.push({
          node,
          exportName: exported.name,
          sourceFile: relativePath,
        });

        const matchedCapabilities = backendImports.filter((entry) => hasWord(exported.body, entry.importName));
        for (const capability of matchedCapabilities) {
          nodes.push(capability.node);
          edges.push({
            id: `delegates_to:${node.id}:${capability.node.id}`,
            kind: "delegates_to",
            from: node.id,
            to: capability.node.id,
            confidence: "high",
          });
        }
      }
    }
  }

  return { nodes: dedupeNodes(nodes), edges, actionEntries };
}

function extractClientRequests(serviceName, filePath, content) {
  const requests = [];
  for (const [functionName, method] of Object.entries(HTTP_WRAPPER_METHODS)) {
    const regex = new RegExp(`\\b${functionName}\\(\\s*["'\\x60]([^"'\\x60]+)["'\\x60]`, "g");
    for (const match of content.matchAll(regex)) {
      const requestPath = normalizeRoute(match[1]);
      if (!requestPath.startsWith("/api/")) continue;
      requests.push({
        method,
        path: requestPath,
        node: {
          id: `client_request:${serviceName}:${filePath}:${method}:${requestPath}`,
          kind: "client_request",
          service: serviceName,
          label: `${method} ${requestPath}`,
          method,
          path: requestPath,
          filePath,
        },
      });
    }
  }

  const fetchRegex =
    /\bfetch\(\s*["'`]([^"'`]+)["'`](?:\s*,\s*\{[\s\S]*?method\s*:\s*["'`](GET|POST|PUT|PATCH|DELETE)["'`][\s\S]*?\})?/gu;
  for (const match of content.matchAll(fetchRegex)) {
    const requestPath = normalizeRoute(match[1]);
    if (!requestPath.startsWith("/api/")) continue;
    const method = match[2] || "GET";
    requests.push({
      method,
      path: requestPath,
      node: {
        id: `client_request:${serviceName}:${filePath}:${method}:${requestPath}`,
        kind: "client_request",
        service: serviceName,
        label: `${method} ${requestPath}`,
        method,
        path: requestPath,
        filePath,
      },
    });
  }

  return dedupeRequests(requests);
}

function extractBackendImports(serviceName, serviceRoot, filePath, content) {
  const imports = [];
  const importRegex = /import\s+\{([^}]+)\}\s+from\s+["'`]([^"'`]+)["'`]/gu;
  for (const match of content.matchAll(importRegex)) {
    const rawNames = match[1]
      .split(",")
      .map((entry) => entry.trim())
      .map((entry) => entry.split(/\s+as\s+/u)[0]?.trim())
      .filter(Boolean);
    const specifier = match[2].trim();
    if (!isBackendSpecifier(specifier)) continue;
    const resolvedFilePath = resolveImportToSourceFile(serviceRoot, filePath, specifier);
    for (const importName of rawNames) {
      imports.push({
        importName,
        node: {
          id: `server_capability:${path.dirname(resolvedFilePath || filePath)}#${importName}`,
          kind: "server_capability",
          service: serviceName,
          label: importName,
          filePath: resolvedFilePath || null,
          metadata: {
            specifier,
          },
        },
      });
    }
  }
  return dedupeBackendImports(imports);
}

function extractExportedMethodBodies(content) {
  const bodies = [];
  for (const method of HTTP_METHODS) {
    const body = extractExportedFunctionBody(content, method);
    if (body) bodies.push([method, body]);
  }
  return bodies;
}

function extractExportedFunctions(content) {
  const exported = [];
  const functionRegex = /export\s+(?:async\s+)?function\s+([A-Za-z0-9_]+)\s*\(/gu;
  for (const match of content.matchAll(functionRegex)) {
    const name = match[1];
    const body = extractExportedFunctionBody(content, name);
    if (!body) continue;
    exported.push({ name, body });
  }
  return exported;
}

function extractPageServerActionRefs(serviceRoot, filePath, content) {
  const refs = [];
  const importRegex = /import\s+\{([^}]+)\}\s+from\s+["'`]([^"'`]+)["'`]/gu;
  for (const match of content.matchAll(importRegex)) {
    const specifier = match[2].trim();
    const resolvedFilePath = resolveImportToSourceFile(serviceRoot, filePath, specifier);
    if (!resolvedFilePath) continue;
    const absoluteResolved = path.join(serviceRoot, resolvedFilePath);
    if (!fs.existsSync(absoluteResolved)) continue;
    const importedContent = fs.readFileSync(absoluteResolved, "utf8");
    if (!isServerActionFile(importedContent)) continue;
    const names = match[1]
      .split(",")
      .map((entry) => entry.trim())
      .map((entry) => entry.split(/\s+as\s+/u)[0]?.trim())
      .filter(Boolean);
    for (const name of names) {
      refs.push({
        exportKey: `${resolvedFilePath}#${name}`,
        confidence: "medium",
      });
    }
  }
  return refs;
}

function extractExportedFunctionBody(content, exportName) {
  const functionStart = new RegExp(`export\\s+(?:async\\s+)?function\\s+${escapeRegExp(exportName)}\\s*\\(`, "u");
  const startMatch = functionStart.exec(content);
  if (!startMatch) return null;
  const afterSignatureIndex = content.indexOf("{", startMatch.index);
  if (afterSignatureIndex === -1) return null;
  return readBalancedBlock(content, afterSignatureIndex);
}

function readBalancedBlock(content, startIndex) {
  let depth = 0;
  let inSingle = false;
  let inDouble = false;
  let inTemplate = false;

  for (let index = startIndex; index < content.length; index += 1) {
    const char = content[index];
    const previous = content[index - 1];
    if (char === "'" && !inDouble && !inTemplate && previous !== "\\") inSingle = !inSingle;
    if (char === '"' && !inSingle && !inTemplate && previous !== "\\") inDouble = !inDouble;
    if (char === "`" && !inSingle && !inDouble && previous !== "\\") inTemplate = !inTemplate;
    if (inSingle || inDouble || inTemplate) continue;
    if (char === "{") depth += 1;
    if (char === "}") {
      depth -= 1;
      if (depth === 0) {
        return content.slice(startIndex, index + 1);
      }
    }
  }
  return null;
}

function inferPageRouteFromTestFile(filePath, nextAppRoot, serviceRoot) {
  const appRelativeSegments = extractRouteOwnerSegments(filePath, nextAppRoot, serviceRoot);
  if (!appRelativeSegments) return null;
  return normalizeRouteSegments(appRelativeSegments);
}

function inferApiRoutesFromTestFile(filePath, nextAppRoot, serviceRoot) {
  const normalized = normalizePath(filePath);
  const appRootRelative = normalizePath(path.relative(serviceRoot, nextAppRoot));
  const appPrefix = appRootRelative === "." ? "" : `${appRootRelative}/`;
  const apiMarker = `${appPrefix}api/`;
  const markerIndex = normalized.indexOf(apiMarker);
  if (markerIndex === -1) return [];
  const rest = normalized.slice(markerIndex + apiMarker.length);
  const segments = rest.split("/");
  const testkitIndex = segments.indexOf("__testkit__");
  if (testkitIndex === -1) return [];
  return [normalizeRouteSegments(segments.slice(0, testkitIndex))];
}

function extractRouteOwnerSegments(filePath, nextAppRoot, serviceRoot) {
  const normalized = normalizePath(filePath);
  const appRootRelative = normalizePath(path.relative(serviceRoot, nextAppRoot));
  const appPrefix = appRootRelative === "." ? "" : `${appRootRelative}/`;
  if (!normalized.startsWith(appPrefix)) return null;
  const rest = normalized.slice(appPrefix.length);
  const segments = rest.split("/");
  const testkitIndex = segments.indexOf("__testkit__");
  if (testkitIndex === -1) return null;
  return segments.slice(0, testkitIndex);
}

function routeFromAppFile(nextAppRoot, filePath) {
  const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(nextAppRoot, filePath);
  const relative = normalizePath(path.relative(nextAppRoot, absolutePath));
  const segments = relative.split("/");
  segments.pop();
  return normalizeRouteSegments(segments);
}

function routeFromApiFile(nextAppRoot, filePath) {
  const appRelative = normalizePath(path.relative(nextAppRoot, path.dirname(filePath)));
  const segments = appRelative.split("/").filter(Boolean);
  if (segments[0] === "api") segments.shift();
  return normalizeRouteSegments(segments);
}

function normalizeRouteSegments(segments) {
  const normalizedSegments = segments
    .filter(Boolean)
    .filter((segment) => !segment.startsWith("(") || !segment.endsWith(")"))
    .filter((segment) => !segment.startsWith("@"))
    .filter((segment) => segment !== "page" && segment !== "route");
  return normalizeRoute(`/${normalizedSegments.join("/")}`);
}

function normalizeRoute(value) {
  const trimmed = String(value || "/").trim();
  if (!trimmed || trimmed === "/") return "/";
  const withLeadingSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
  return withLeadingSlash.length > 1 ? withLeadingSlash.replace(/\/+$/u, "") : withLeadingSlash;
}

function findNextAppRoot(serviceRoot) {
  const candidates = [path.join(serviceRoot, "app"), path.join(serviceRoot, "src", "app")];
  return candidates.find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) || null;
}

function resolveServiceRoot(productDir, config) {
  const cwd = config?.local?.cwd || ".";
  return path.resolve(productDir, cwd);
}

function walkFiles(rootDir, options = {}) {
  if (!fs.existsSync(rootDir)) return [];
  const baseDir = options.baseDir || rootDir;
  const exclude = options.exclude || [];
  const results = [];
  const queue = [rootDir];
  while (queue.length > 0) {
    const current = queue.pop();
    for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
      if (entry.isSymbolicLink()) continue;
      const absolutePath = path.join(current, entry.name);
      if (entry.isDirectory()) {
        const relativeDirPath = normalizeDiscoveryPath(path.relative(baseDir, absolutePath));
        if (shouldExcludeDiscoveryPath(relativeDirPath, exclude)) continue;
        queue.push(absolutePath);
        continue;
      }
      if (entry.isFile()) {
        results.push(absolutePath);
      }
    }
  }
  return results.sort((left, right) => left.localeCompare(right));
}

function resolveImportToSourceFile(serviceRoot, fromFilePath, specifier) {
  const candidates = [];
  const sourceRoot = fs.existsSync(path.join(serviceRoot, "src")) ? path.join(serviceRoot, "src") : serviceRoot;
  if (specifier.startsWith("@/")) {
    candidates.push(path.join(sourceRoot, specifier.slice(2)));
  } else if (specifier.startsWith("./") || specifier.startsWith("../")) {
    candidates.push(path.join(path.dirname(path.join(serviceRoot, fromFilePath)), specifier));
  } else {
    return null;
  }

  for (const candidate of candidates) {
    const resolved = resolveSourceCandidate(candidate);
    if (resolved) return normalizePath(path.relative(serviceRoot, resolved));
  }
  return null;
}

function resolveSourceCandidate(basePath) {
  const direct = [basePath, `${basePath}.ts`, `${basePath}.tsx`, `${basePath}.js`, `${basePath}.mjs`];
  for (const candidate of direct) {
    if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
  }
  const indexed = [
    path.join(basePath, "index.ts"),
    path.join(basePath, "index.tsx"),
    path.join(basePath, "index.js"),
    path.join(basePath, "index.mjs"),
  ];
  for (const candidate of indexed) {
    if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
  }
  return null;
}

function isServerActionFile(content) {
  const trimmed = content.trimStart();
  return trimmed.startsWith('"use server"') || trimmed.startsWith("'use server'");
}

function isBackendSpecifier(specifier) {
  return (
    specifier.startsWith("@/backend/server/") ||
    specifier.includes("/backend/server/") ||
    specifier.startsWith("./backend/server/") ||
    specifier.startsWith("../backend/server/")
  );
}

function hasWord(content, word) {
  if (!content || !word) return false;
  return new RegExp(`\\b${escapeRegExp(word)}\\b`, "u").test(content);
}

function pageLabelFromRoute(route) {
  if (route === "/") return "Home";
  return route
    .split("/")
    .filter(Boolean)
    .map((segment) => {
      if (segment.startsWith("[") && segment.endsWith("]")) {
        return segment.slice(1, -1);
      }
      return segment;
    })
    .map((segment) => segment.replace(/[-_]+/gu, " "))
    .map((segment) => segment.replace(/^\w/u, (char) => char.toUpperCase()))
    .join(" ");
}

function apiRouteLookupKey(method, route) {
  return `${method}:${route}`;
}

function toApiRequestPath(route) {
  return normalizeRoute(`/api${route === "/" ? "" : route}`);
}

function toSelectionType(type, framework) {
  if (framework === "playwright") return "pw";
  if (type === "integration") return "int";
  return type;
}

function normalizePath(filePath) {
  return filePath.split(path.sep).join("/");
}

function escapeRegExp(value) {
  return String(value).replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
}

function dedupeRequests(requests) {
  const seen = new Set();
  return requests.filter((entry) => {
    const key = `${entry.method}:${entry.path}`;
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
}

function dedupeNodes(nodes) {
  const seen = new Set();
  return nodes.filter((node) => {
    if (seen.has(node.id)) return false;
    seen.add(node.id);
    return true;
  });
}

function dedupeBackendImports(entries) {
  const seen = new Set();
  return entries.filter((entry) => {
    const key = `${entry.importName}:${entry.node.filePath || ""}:${entry.node.label}`;
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
}
