import { discoverDataCapabilities } from "./backend-discovery.mjs";
import { buildEvidenceDetails, inferCoveredNodeIdsForTest } from "./evidence.mjs";
import { findNextAppRoot, resolveImportToSourceFile, resolveServiceRoot } from "./fs-walk.mjs";
import { discoverApiRoutes, discoverPageViews, discoverServerActions } from "./next-discovery.mjs";
import {
  appendGraph,
  createEmptyGraph,
  createTestFileNode,
  findMatchingApiRouteEntry,
  normalizePath,
  toSelectionType,
} from "./shared.mjs";
import { DEFAULT_DISCOVERY_EXCLUDES, normalizeDiscoveryConfig } from "../discovery/path-policy.mjs";

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) {
      graph.diagnostics.push({
        level: "warn",
        code: "no-service-context",
        filePath: entry.filePath,
        service: entry.serviceName,
        message: `No coverage context available for service "${entry.serviceName}".`,
      });
      evidence.push(createFallbackEvidence(entry, testNodeId));
      continue;
    }

    const coveredNodeIds = inferCoveredNodeIdsForTest(entry, context);
    if (coveredNodeIds.length === 0) {
      graph.diagnostics.push({
        level: "info",
        code: "zero-coverage-inferred",
        filePath: entry.filePath,
        service: entry.serviceName,
        message: `No routes, API endpoints, or data capabilities matched for "${entry.filePath}".`,
      });
      evidence.push(createFallbackEvidence(entry, 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, entry, context),
    });
  }

  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 discoveryOptions = {
    serviceName,
    serviceRoot,
    nextAppRoot,
    exclude,
    resolveImportToSourceFile,
  };
  const pages = discoverPageViews(discoveryOptions);
  const apiRoutes = discoverApiRoutes(discoveryOptions);
  const serverActions = discoverServerActions({
    serviceName,
    serviceRoot,
    exclude,
    resolveImportToSourceFile,
  });

  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 dataAugmentation = discoverDataCapabilities({ serviceName, serviceRoot, nodes: graph.nodes });
  for (const node of dataAugmentation.nodes) graph.nodes.push(node);
  for (const edge of dataAugmentation.edges) graph.edges.push(edge);

  const pageByRoute = new Map(pages.pageEntries.map((entry) => [entry.route, entry]));
  const apiRouteByKey = new Map(
    apiRoutes.routeEntries.map((entry) => [`${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 = findMatchingApiRouteEntry(request.method, request.path, apiRoutes.routeEntries);
      if (!apiRoute) continue;
      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:${actionRef.originNodeId}:${serverAction.node.id}`,
        kind: "triggers",
        from: actionRef.originNodeId,
        to: serverAction.node.id,
        confidence: actionRef.confidence,
      });
    }
  }

  return {
    serviceName,
    serviceRoot,
    nextAppRoot,
    graph,
    pageByRoute,
    apiRouteByKey,
    apiRouteEntries: apiRoutes.routeEntries,
    serverActionByExportKey,
    dataCapabilities: dataAugmentation.nodes,
  };
}

function createFallbackEvidence(entry, testNodeId) {
  return {
    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],
  };
}
