import { createRequire } from "module";
import http from "http";
import {
  TESTKIT_BROWSER_PROTOCOL_VERSION,
  createBridgeErrorResponse,
} from "@elench/testkit-protocol";

const require = createRequire(import.meta.url);
const BRIDGE_PACKAGE_VERSION = require("../package.json").version;

export function createBrowserBridge(adapter) {
  if (!adapter || typeof adapter.loadProductContext !== "function") {
    throw new Error("createBrowserBridge(adapter) requires a loadProductContext function");
  }

  return {
    async handleRequest(request, response) {
      try {
        const url = new URL(request.url || "/", "http://127.0.0.1");
        const context = await adapter.loadProductContext();

        if (request.method === "OPTIONS") {
          response.statusCode = 204;
          response.setHeader("access-control-allow-origin", "*");
          response.setHeader("access-control-allow-methods", "GET, OPTIONS");
          response.setHeader("access-control-allow-headers", "content-type");
          response.end();
          return;
        }

        if (request.method !== "GET") {
          return writeJson(response, 405, createBridgeErrorResponse("method_not_allowed", "Only GET is supported"));
        }

        if (url.pathname === "/health") {
          return writeJson(response, 200, buildHealthResponse(context));
        }

        if (url.pathname === "/match") {
          const pageUrl = url.searchParams.get("url");
          if (!pageUrl) {
            return writeJson(response, 400, createBridgeErrorResponse("missing_url", 'Expected query parameter "url"'));
          }
          return writeJson(response, 200, buildMatchResponse(context, pageUrl));
        }

        if (url.pathname === "/page" || url.pathname === "/failures" || url.pathname === "/coverage") {
          const pageUrl = url.searchParams.get("url");
          if (!pageUrl) {
            return writeJson(response, 400, createBridgeErrorResponse("missing_url", 'Expected query parameter "url"'));
          }
          const payload = buildPageOverlayResponse(context, pageUrl);
          if (url.pathname === "/failures") {
            return writeJson(response, 200, {
              protocolVersion: payload.protocolVersion,
              page: payload.page,
              match: payload.match,
              run: payload.run,
              summary: payload.summary,
              failures: payload.failures,
            });
          }
          if (url.pathname === "/coverage") {
            return writeJson(response, 200, {
              protocolVersion: payload.protocolVersion,
              page: payload.page,
              match: payload.match,
              run: payload.run,
              summary: payload.summary,
              coverage: payload.coverage,
            });
          }
          return writeJson(response, 200, payload);
        }

        return writeJson(response, 404, createBridgeErrorResponse("not_found", `No bridge endpoint for ${url.pathname}`));
      } catch (error) {
        return writeJson(
          response,
          500,
          createBridgeErrorResponse("internal_error", error instanceof Error ? error.message : String(error))
        );
      }
    },
  };
}

export function startBrowserBridgeServer(adapter, options = {}) {
  const bridge = createBrowserBridge(adapter);
  const host = options.host || "127.0.0.1";
  const port = Number(options.port || 3847);
  const server = http.createServer((request, response) => {
    bridge.handleRequest(request, response);
  });

  return new Promise((resolve, reject) => {
    server.once("error", reject);
    server.listen(port, host, () => {
      server.removeListener("error", reject);
      resolve({
        server,
        host,
        port,
        url: `http://${host}:${port}`,
      });
    });
  });
}

export function buildHealthResponse(context) {
  return {
    protocolVersion: TESTKIT_BROWSER_PROTOCOL_VERSION,
    bridgeVersion: BRIDGE_PACKAGE_VERSION,
    status: "ok",
    product: context.product,
  };
}

export function buildMatchResponse(context, pageUrl) {
  const page = normalizePage(pageUrl);
  const service = matchServiceForPage(context.services || [], page);

  return {
    protocolVersion: TESTKIT_BROWSER_PROTOCOL_VERSION,
    url: page.url,
    origin: page.origin,
    route: page.route,
    matched: Boolean(service),
    product: context.product,
    service: service
      ? {
          name: service.name,
          baseUrl: service.baseUrl,
          origin: service.origin,
        }
      : null,
  };
}

export function buildPageOverlayResponse(context, pageUrl) {
  const page = normalizePage(pageUrl);
  const match = buildMatchResponse(context, page.url);
  const projection = buildGraphProjection(context, page, match.service?.name || null);
  const relatedCoverageCount = projection.coverage.length;
  const relatedFailureCount = projection.failures.length;
  const coverageBreakdown = buildCoverageBreakdown(projection.coverage);

  return {
    protocolVersion: TESTKIT_BROWSER_PROTOCOL_VERSION,
    page,
    match,
    run: {
      artifactAvailable: Boolean(context.runArtifact),
      generatedAt: context.runArtifact?.generatedAt || null,
      status: context.runArtifact?.run?.status || null,
    },
    summary: {
      failureState: context.runArtifact
        ? relatedFailureCount > 0
          ? "failing"
          : "healthy"
        : "unavailable",
      coverageState: relatedCoverageCount > 0 ? "covered" : "missing",
      relatedFailureCount,
      relatedCoverageCount,
      coverageBreakdown,
    },
    failures: projection.failures,
    coverage: projection.coverage,
  };
}

function collectFailedFiles(runArtifact) {
  if (!runArtifact || !Array.isArray(runArtifact.services)) return [];
  return runArtifact.services.flatMap((service) =>
    (service.suites || []).flatMap((suite) =>
      (suite.files || [])
        .filter((file) => file.status === "failed")
        .map((file) => ({
          service: service.name,
          suite: suite.name,
          type: suite.type,
          framework: suite.framework,
          filePath: file.path,
          error: file.error || null,
          failureDetails: Array.isArray(file.failureDetails) ? file.failureDetails : [],
        }))
    )
  );
}

function matchServiceForPage(services, page) {
  return [...services]
    .filter((service) => matchesServiceBaseUrl(service.baseUrl, page))
    .sort((left, right) => normalizeBaseUrl(right.baseUrl).length - normalizeBaseUrl(left.baseUrl).length)[0] || null;
}

function matchesServiceBaseUrl(baseUrl, page) {
  const normalized = normalizeBaseUrl(baseUrl);
  if (!normalized) return false;
  if (!page.url.startsWith(normalized)) return false;
  const parsed = new URL(normalized);
  const basePath = normalizeRoute(parsed.pathname || "/");
  return basePath === "/" || page.route === basePath || page.route.startsWith(`${basePath}/`);
}

function normalizePage(value) {
  const parsed = new URL(String(value));
  return {
    url: parsed.href,
    origin: parsed.origin,
    route: normalizeRoute(parsed.pathname || "/"),
  };
}

function normalizeBaseUrl(value) {
  if (typeof value !== "string" || value.trim().length === 0) return null;
  try {
    const parsed = new URL(value);
    const normalizedPath = normalizeRoute(parsed.pathname || "/");
    return `${parsed.origin}${normalizedPath === "/" ? "" : normalizedPath}`;
  } catch {
    return null;
  }
}

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 writeJson(response, statusCode, payload) {
  response.statusCode = statusCode;
  response.setHeader("content-type", "application/json; charset=utf-8");
  response.setHeader("access-control-allow-origin", "*");
  response.setHeader("access-control-allow-methods", "GET, OPTIONS");
  response.setHeader("access-control-allow-headers", "content-type");
  response.end(`${JSON.stringify(payload, null, 2)}\n`);
}

function buildGraphProjection(context, page, matchedServiceName) {
  const graph = context.discovery?.coverageGraph || null;
  if (!graph || !matchedServiceName) {
    return { coverage: [], failures: [] };
  }

  const nodes = Array.isArray(graph.nodes) ? graph.nodes : [];
  const edges = Array.isArray(graph.edges) ? graph.edges : [];
  const evidence = Array.isArray(graph.evidence) ? graph.evidence : [];
  const discoveryFiles = Array.isArray(context.discovery?.files) ? context.discovery.files : [];
  const discoveryByFile = new Map(discoveryFiles.map((entry) => [entry.path, entry]));
  const nodeById = new Map(nodes.map((node) => [node.id, node]));
  const outgoing = buildOutgoingEdgeMap(edges);
  const pageNode = nodes.find(
    (node) =>
      node.kind === "page_view" && node.service === matchedServiceName && normalizeRoute(node.route || "/") === page.route
  );

  if (!pageNode) {
    return { coverage: [], failures: [] };
  }

  const failedFiles = collectFailedFiles(context.runArtifact);
  const failureByFile = new Map(failedFiles.map((entry) => [entry.filePath, entry]));
  const surfaceNodes = (outgoing.get(pageNode.id) || [])
    .map((nodeId) => nodeById.get(nodeId))
    .filter((node) => node?.kind === "ui_surface");
  const pageReachableNodeIds = collectReachableNodeIds(pageNode.id, outgoing);

  const surfaceReachableEntries = surfaceNodes.map((surfaceNode) => ({
    surfaceNode,
    reachableNodeIds: collectReachableNodeIds(surfaceNode.id, outgoing),
  }));

  if (surfaceNodes.length === 0) {
    const relevantEvidence = evidence.filter((entry) => intersects(entry.coveredNodeIds || [], pageReachableNodeIds));
    return buildPageLevelProjection({
      pageNode,
      nodeById,
      relevantEvidence,
      pageReachableNodeIds,
      discoveryByFile,
      failureByFile,
      runArtifact: context.runArtifact,
    });
  }

  const coverage = [];
  const failures = [];

  for (const { surfaceNode, reachableNodeIds: surfaceReachableNodeIds } of surfaceReachableEntries.sort((left, right) =>
    left.surfaceNode.id.localeCompare(right.surfaceNode.id)
  )) {
    const relevantEvidence = evidence.filter((entry) => intersects(entry.coveredNodeIds || [], surfaceReachableNodeIds));
    const supportingTests = relevantEvidence
      .map((entry) => buildSupportingTestRef(entry, discoveryByFile, context.runArtifact))
      .filter(Boolean);

    if (supportingTests.length > 0) {
      const supportKind = inferCoverageSupportKind(supportingTests);
      coverage.push({
        id: surfaceNode.id,
        kind: surfaceNode.kind,
        label: surfaceNode.label,
        service: surfaceNode.service,
        route: pageNode.route || null,
        targets: collectTargetsForEvidence(relevantEvidence, surfaceNode.target),
        supportingTests,
        viaNodes: collectViaNodes(relevantEvidence, surfaceReachableNodeIds, nodeById, new Set([pageNode.id, surfaceNode.id])),
        confidence: supportingTests.some((entry) => entry.type === "pw") ? "high" : "medium",
        importance: inferSurfaceImportance(surfaceNode),
        surfaceKind: surfaceNode.metadata?.surfaceKind ? String(surfaceNode.metadata.surfaceKind) : null,
        supportKind,
        reason: buildCoverageReason(surfaceNode, supportKind, supportingTests, relevantEvidence),
      });
    }

    const failedEvidence = relevantEvidence.filter((entry) => failureByFile.has(entry.testFilePath));
    if (failedEvidence.length === 0) continue;

    const failedTests = failedEvidence
      .map((entry) => {
        const supporting = buildSupportingTestRef(entry, discoveryByFile, context.runArtifact);
        const failed = failureByFile.get(entry.testFilePath);
        if (!supporting) return null;
        return {
          ...supporting,
          error: failed?.error || supporting.error || null,
          status: "failed",
        };
      })
      .filter(Boolean);

    failures.push({
      id: `failure:${surfaceNode.id}`,
      kind: surfaceNode.kind,
      label: surfaceNode.label,
      service: surfaceNode.service,
      route: pageNode.route || null,
      targets: collectTargetsForEvidence(failedEvidence, surfaceNode.target),
      failedTests,
      viaNodes: collectViaNodes(failedEvidence, surfaceReachableNodeIds, nodeById, new Set([pageNode.id, surfaceNode.id])),
      importance: inferSurfaceImportance(surfaceNode),
      surfaceKind: surfaceNode.metadata?.surfaceKind ? String(surfaceNode.metadata.surfaceKind) : null,
      reason: buildFailureReason(surfaceNode, failedTests),
    });
  }

  const routeLevelNodeIds = collectRouteLevelNodeIds(pageReachableNodeIds, surfaceReachableEntries);
  if (routeLevelNodeIds.size > 0) {
    const routeRelevantEvidence = evidence.filter((entry) => intersects(entry.coveredNodeIds || [], routeLevelNodeIds));
    const pageProjection = buildPageLevelProjection({
      pageNode,
      nodeById,
      relevantEvidence: routeRelevantEvidence,
      pageReachableNodeIds: routeLevelNodeIds,
      discoveryByFile,
      failureByFile,
      runArtifact: context.runArtifact,
    });
    coverage.unshift(...pageProjection.coverage);
    failures.unshift(...pageProjection.failures);
  }

  return { coverage, failures };
}

function buildPageLevelProjection({ pageNode, nodeById, relevantEvidence, pageReachableNodeIds, discoveryByFile, failureByFile, runArtifact }) {
  const supportingTests = relevantEvidence
    .map((entry) => buildSupportingTestRef(entry, discoveryByFile, runArtifact))
    .filter(Boolean);

  const coverage = supportingTests.length > 0
    ? [
        {
          id: pageNode.id,
          kind: pageNode.kind,
          label: pageNode.label,
          service: pageNode.service,
          route: pageNode.route || null,
          targets: collectTargetsForEvidence(relevantEvidence, pageNode.target),
          supportingTests,
          viaNodes: collectViaNodes(relevantEvidence, pageReachableNodeIds, nodeById, new Set([pageNode.id])),
          confidence: supportingTests.some((entry) => entry.type === "pw") ? "high" : "medium",
          importance: inferSurfaceImportance(pageNode),
          surfaceKind: pageNode.kind,
          supportKind: inferCoverageSupportKind(supportingTests),
          reason: buildCoverageReason(pageNode, inferCoverageSupportKind(supportingTests), supportingTests, relevantEvidence),
        },
      ]
    : [];

  const failures = relevantEvidence
    .filter((entry) => failureByFile.has(entry.testFilePath))
    .map((entry) => {
      const failed = failureByFile.get(entry.testFilePath);
      const supporting = buildSupportingTestRef(entry, discoveryByFile, runArtifact);
      return {
        id: `failure:${entry.testFilePath}`,
        kind: pageNode.kind,
        label: supporting?.label || pageNode.label,
        service: pageNode.service,
        route: pageNode.route || null,
        targets: collectTargetsForEvidence([entry], pageNode.target),
        failedTests: supporting ? [{ ...supporting, error: failed?.error || supporting.error || null, status: "failed" }] : [],
        viaNodes: collectViaNodes([entry], pageReachableNodeIds, nodeById, new Set([pageNode.id])),
        importance: inferSurfaceImportance(pageNode),
        surfaceKind: pageNode.kind,
        reason: buildFailureReason(pageNode, supporting ? [{ ...supporting, error: failed?.error || supporting.error || null, status: "failed" }] : []),
      };
    });

  return { coverage, failures };
}

function buildOutgoingEdgeMap(edges) {
  const map = new Map();
  for (const edge of edges || []) {
    const entries = map.get(edge.from) || [];
    entries.push(edge.to);
    map.set(edge.from, entries);
  }
  return map;
}

function collectReachableNodeIds(startNodeId, outgoing) {
  const visited = new Set([startNodeId]);
  const queue = [startNodeId];
  while (queue.length > 0) {
    const current = queue.shift();
    for (const next of outgoing.get(current) || []) {
      if (visited.has(next)) continue;
      visited.add(next);
      queue.push(next);
    }
  }
  return visited;
}

function collectRouteLevelNodeIds(pageReachableNodeIds, surfaceReachableEntries) {
  const routeLevelNodeIds = new Set(pageReachableNodeIds);
  for (const { reachableNodeIds } of surfaceReachableEntries) {
    for (const nodeId of reachableNodeIds) {
      routeLevelNodeIds.delete(nodeId);
    }
  }
  return routeLevelNodeIds;
}

function collectViaNodes(evidenceEntries, reachableNodeIds, nodeById, excludedNodeIds = new Set()) {
  const nodes = new Map();
  for (const entry of evidenceEntries) {
    for (const nodeId of entry.coveredNodeIds || []) {
      if (!reachableNodeIds.has(nodeId) || excludedNodeIds.has(nodeId)) continue;
      const node = nodeById.get(nodeId);
      if (!node) continue;
      nodes.set(nodeId, {
        id: node.id,
        kind: node.kind,
        label: node.label,
        ...(node.route ? { route: node.route } : {}),
        ...(node.method ? { method: node.method } : {}),
        ...(node.path ? { path: node.path } : {}),
      });
    }
  }
  return [...nodes.values()].sort((left, right) => left.id.localeCompare(right.id));
}

function collectTargetsForEvidence(evidenceEntries, pageTarget) {
  const targets = [];
  if (pageTarget) targets.push(pageTarget);
  for (const entry of evidenceEntries || []) {
    for (const target of entry?.details?.targets || []) {
      targets.push(target);
    }
  }
  const seen = new Set();
  return targets.filter((target) => {
    const key = `${target.kind}:${target.value}`;
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
}

function buildSupportingTestRef(evidence, discoveryByFile, runArtifact) {
  const discovery = discoveryByFile.get(evidence.testFilePath);
  const runFile = findRunFileResult(runArtifact, evidence.testFilePath);
  return {
    service: evidence.service,
    suite: evidence.suiteName,
    type: evidence.selectionType,
    framework: evidence.framework,
    filePath: evidence.testFilePath,
    label: discovery?.displayName || pathBaseName(evidence.testFilePath),
    ...(runFile?.status ? { status: runFile.status } : {}),
    ...(runFile?.error ? { error: runFile.error } : {}),
  };
}

function findRunFileResult(runArtifact, targetPath) {
  if (!runArtifact || !Array.isArray(runArtifact.services)) return null;
  for (const service of runArtifact.services) {
    for (const suite of service.suites || []) {
      for (const file of suite.files || []) {
        if (file.path === targetPath) return file;
      }
    }
  }
  return null;
}

function intersects(values, setLike) {
  for (const value of values || []) {
    if (setLike.has(value)) return true;
  }
  return false;
}

function pathBaseName(filePath) {
  const parts = String(filePath || "").split("/");
  return parts[parts.length - 1] || filePath;
}

function buildCoverageBreakdown(entries) {
  const breakdown = { direct: 0, indirect: 0, mixed: 0 };
  for (const entry of entries || []) {
    if (entry.supportKind === "direct") breakdown.direct += 1;
    else if (entry.supportKind === "indirect") breakdown.indirect += 1;
    else breakdown.mixed += 1;
  }
  return breakdown;
}

function inferCoverageSupportKind(supportingTests) {
  const hasPw = (supportingTests || []).some((entry) => entry.type === "pw");
  const hasBackend = (supportingTests || []).some((entry) => entry.type !== "pw");
  if (hasPw && hasBackend) return "mixed";
  if (hasPw) return "direct";
  return "indirect";
}

function inferSurfaceImportance(node) {
  const label = String(node?.label || "").toLowerCase();
  const surfaceKind = String(node?.metadata?.surfaceKind || node?.kind || "").toLowerCase();

  if (/\b(pay|purchase|checkout|publish|send|submit|confirm|delete|remove)\b/u.test(label)) {
    return "critical";
  }
  if (/\b(save|create|update|refresh|retry|login|sign in|continue)\b/u.test(label)) {
    return "high";
  }
  if (surfaceKind === "form" || surfaceKind === "button" || surfaceKind === "input") {
    return "medium";
  }
  return "low";
}

function buildCoverageReason(node, supportKind, supportingTests, evidenceEntries) {
  const directCount = supportingTests.filter((entry) => entry.type === "pw").length;
  const backendCount = supportingTests.filter((entry) => entry.type !== "pw").length;
  const requestPaths = new Set();
  for (const entry of evidenceEntries || []) {
    for (const path of entry?.details?.requestPaths || []) requestPaths.add(path);
  }
  const requestSummary = requestPaths.size > 0 ? ` via ${[...requestPaths].join(", ")}` : "";
  if (supportKind === "mixed") {
    return `${node.label} is covered directly by ${directCount} UI test${directCount === 1 ? "" : "s"} and indirectly by ${backendCount} backend test${backendCount === 1 ? "" : "s"}${requestSummary}.`;
  }
  if (supportKind === "direct") {
    return `${node.label} is covered directly by ${directCount} UI test${directCount === 1 ? "" : "s"}${requestSummary}.`;
  }
  return `${node.label} is covered indirectly by ${backendCount} backend test${backendCount === 1 ? "" : "s"}${requestSummary}.`;
}

function buildFailureReason(node, failedTests) {
  const count = (failedTests || []).length;
  if (count === 0) return `${node.label} has a related failing test.`;
  return `${node.label} is implicated by ${count} failing test${count === 1 ? "" : "s"}.`;
}
