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;

  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,
    },
    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 reachableNodeIds = collectReachableNodeIds(pageNode.id, outgoing);
  const relevantEvidence = evidence.filter((entry) =>
    intersects(entry.coveredNodeIds || [], reachableNodeIds)
  );
  const supportingTests = relevantEvidence
    .map((entry) => buildSupportingTestRef(entry, discoveryByFile, context.runArtifact))
    .filter(Boolean);

  const viaNodes = collectViaNodes(relevantEvidence, reachableNodeIds, nodeById, pageNode.id);
  const coverage = supportingTests.length > 0
    ? [
        {
          id: pageNode.id,
          kind: pageNode.kind,
          label: pageNode.label,
          service: pageNode.service,
          route: pageNode.route || null,
          targets: pageNode.target ? [pageNode.target] : [],
          supportingTests,
          viaNodes,
          confidence: supportingTests.some((entry) => entry.type === "pw") ? "high" : "medium",
        },
      ]
    : [];

  const failedFiles = collectFailedFiles(context.runArtifact);
  const failureByFile = new Map(failedFiles.map((entry) => [entry.filePath, entry]));
  const failures = relevantEvidence
    .filter((entry) => failureByFile.has(entry.testFilePath))
    .map((entry) => {
      const failed = failureByFile.get(entry.testFilePath);
      const supporting = buildSupportingTestRef(entry, discoveryByFile, context.runArtifact);
      return {
        id: `failure:${entry.testFilePath}`,
        kind: pageNode.kind,
        label: supporting?.label || pageNode.label,
        service: pageNode.service,
        route: pageNode.route || null,
        targets: pageNode.target ? [pageNode.target] : [],
        failedTests: supporting ? [{ ...supporting, error: failed?.error || supporting.error || null, status: "failed" }] : [],
        viaNodes: collectViaNodes([entry], reachableNodeIds, nodeById, pageNode.id),
      };
    });

  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 collectViaNodes(evidenceEntries, reachableNodeIds, nodeById, pageNodeId) {
  const nodes = new Map();
  for (const entry of evidenceEntries) {
    for (const nodeId of entry.coveredNodeIds || []) {
      if (!reachableNodeIds.has(nodeId) || nodeId === pageNodeId) 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 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;
}
