import path from "path";
import ts from "typescript";
import { DYNAMIC_SEGMENT_TOKEN } from "./shared.mjs";

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

const SURFACE_COMPONENT_NAMES = new Set(["button", "Button", "form", "Form", "a", "Link", "input"]);
const SURFACE_ACTION_PROPS = new Set(["onClick", "action", "onSubmit"]);

export function analyzeNextPageFile({
  serviceName,
  serviceRoot,
  filePath,
  route,
  content,
  readSourceFile,
  resolveImportPath,
  isServerActionFile,
  normalizeRoute,
}) {
  return analyzeNextRouteTree({
    serviceName,
    serviceRoot,
    route,
    rootFiles: [{ filePath, content }],
    readSourceFile,
    resolveImportPath: (fromFilePath, specifier) =>
      fromFilePath === filePath ? resolveImportPath(specifier) : resolveImportToSourceFile(serviceRoot, fromFilePath, specifier),
    isServerActionFile,
    normalizeRoute,
  });
}

export function analyzeNextRouteTree({
  serviceName,
  serviceRoot,
  route,
  rootFiles = [],
  readSourceFile,
  resolveImportPath,
  isServerActionFile,
  normalizeRoute,
}) {
  const directRequests = [];
  const directServerActionRefs = [];
  const nodes = [];
  const edges = [];
  const surfacesByTargetValue = new Map();
  const actionNodeIds = new Set();
  const requestNodeIds = new Set();
  const pageNodeId = `page_view:${serviceName}:${route}`;
  const moduleCache = new Map();
  const visitedExports = new Set();

  const analyzerContext = {
    serviceName,
    serviceRoot,
    route,
    pageNodeId,
    readSourceFile,
    resolveImportPath,
    isServerActionFile,
    normalizeRoute,
    moduleCache,
    visitedExports,
    state: {
      nodes,
      edges,
      directRequests,
      directServerActionRefs,
      surfacesByTargetValue,
      actionNodeIds,
      requestNodeIds,
    },
  };

  for (const rootFile of rootFiles) {
    const rootContent =
      typeof rootFile.content === "string"
        ? rootFile.content
        : readSourceFile(rootFile.filePath);
    if (!rootContent) continue;
    const moduleInfo = loadModuleInfo(rootFile.filePath, rootContent, analyzerContext);
    analyzeModuleExport(moduleInfo, "default", analyzerContext);
  }

  return {
    nodes: dedupeById(nodes),
    edges: dedupeById(edges),
    requests: directRequests,
    serverActionRefs: dedupeServerActionRefs(directServerActionRefs),
    surfacesByTargetValue,
  };
}

export function extractHttpSuiteRequests(content, filePath = "suite.int.testkit.ts") {
  const sourceFile = createSourceFile(filePath, content);
  const requests = [];

  const visit = (node) => {
    if (ts.isCallExpression(node)) {
      const request = resolveHttpRequestCall(node, { normalizeRoute: defaultNormalizeRoute });
      if (request) requests.push(request);
    }
    ts.forEachChild(node, visit);
  };

  visit(sourceFile);
  return dedupeRequests(requests);
}

export function extractPlaywrightVisitedRoutes(content, filePath = "suite.pw.testkit.ts") {
  const sourceFile = createSourceFile(filePath, content);
  const routes = [];

  const visit = (node) => {
    if (ts.isCallExpression(node)) {
      const route = resolveGotoRoute(node);
      if (route) routes.push(route);
    }
    ts.forEachChild(node, visit);
  };

  visit(sourceFile);
  return [...new Set(routes)];
}

function analyzeModuleExport(moduleInfo, contextExportName, analyzerContext) {
  if (!moduleInfo) return;
  const visitKey = `${moduleInfo.filePath}#${contextExportName}`;
  if (analyzerContext.visitedExports.has(visitKey)) return;
  analyzerContext.visitedExports.add(visitKey);

  const callable = resolveModuleExportCallable(moduleInfo, contextExportName);
  if (!callable) return;

  const localFunctions = new Map(moduleInfo.topLevelFunctions);
  for (const [name, node] of collectScopeLocalFunctions(callable)) {
    localFunctions.set(name, node);
  }

  const actionAnalysis = analyzeCallableLikeNode(
    callable,
    {
      imports: moduleInfo.imports,
      localFunctions,
      normalizeRoute: analyzerContext.normalizeRoute,
      moduleLoader: (filePath) => loadModuleInfoFromFile(filePath, analyzerContext),
      resolveImportedExport: resolveImportedExportCallable,
      analyzeImportedCallable,
    },
    new Set(),
    { skipNestedFunctions: true }
  );

  for (const request of actionAnalysis.requests) {
    const node = createClientRequestNode(
      analyzerContext.serviceName,
      moduleInfo.filePath,
      request,
      `page:${analyzerContext.route}:${moduleInfo.filePath}`
    );
    if (!analyzerContext.state.requestNodeIds.has(node.id)) {
      analyzerContext.state.requestNodeIds.add(node.id);
      analyzerContext.state.nodes.push(node);
    }
    analyzerContext.state.directRequests.push({
      originNodeId: analyzerContext.pageNodeId,
      node,
      method: request.method,
      path: request.path,
    });
    analyzerContext.state.edges.push({
      id: `requests:${analyzerContext.pageNodeId}:${node.id}`,
      kind: "requests",
      from: analyzerContext.pageNodeId,
      to: node.id,
      confidence: request.confidence,
    });
  }

  for (const serverActionRef of actionAnalysis.serverActionRefs) {
    analyzerContext.state.directServerActionRefs.push({
      originNodeId: analyzerContext.pageNodeId,
      ...serverActionRef,
    });
  }

  walkJsx(callable, (element, formContext) => {
    const descriptor = describeSurface(element, formContext);
    if (descriptor) {
      const surfaceKey = descriptor.target?.value || descriptor.actionName || `${moduleInfo.filePath}:${descriptor.tagName}:${descriptor.line}`;
      const surfaceId = `ui_surface:${analyzerContext.serviceName}:${analyzerContext.route}:${surfaceKey}`;
      const surfaceNode = {
        id: surfaceId,
        kind: "ui_surface",
        service: analyzerContext.serviceName,
        label: descriptor.label,
        route: analyzerContext.route,
        filePath: moduleInfo.filePath,
        ...(descriptor.target ? { target: descriptor.target } : {}),
        metadata: {
          surfaceKind: descriptor.surfaceKind,
          tagName: descriptor.tagName,
        },
      };

      analyzerContext.state.nodes.push(surfaceNode);
      analyzerContext.state.edges.push({
        id: `contains:${analyzerContext.pageNodeId}:${surfaceId}`,
        kind: "contains",
        from: analyzerContext.pageNodeId,
        to: surfaceId,
        confidence: "high",
      });

      if (descriptor.target?.kind === "testId") {
        analyzerContext.state.surfacesByTargetValue.set(descriptor.target.value, surfaceNode);
      }

      if (descriptor.actionBinding) {
        const actionId = `ui_action:${analyzerContext.serviceName}:${analyzerContext.route}:${descriptor.actionBinding.key}`;
        if (!analyzerContext.state.actionNodeIds.has(actionId)) {
          analyzerContext.state.actionNodeIds.add(actionId);
          analyzerContext.state.nodes.push({
            id: actionId,
            kind: "ui_action",
            service: analyzerContext.serviceName,
            label: descriptor.actionBinding.label,
            route: analyzerContext.route,
            filePath: moduleInfo.filePath,
            metadata: {
              bindingKind: descriptor.actionBinding.kind,
              actionProp: descriptor.actionBinding.actionProp,
            },
          });
        }

        analyzerContext.state.edges.push({
          id: `triggers:${surfaceId}:${actionId}`,
          kind: "triggers",
          from: surfaceId,
          to: actionId,
          confidence: descriptor.actionBinding.confidence,
        });

        const bindingAnalysis = analyzeActionBinding(descriptor.actionBinding, {
          imports: moduleInfo.imports,
          localFunctions,
          normalizeRoute: analyzerContext.normalizeRoute,
          moduleLoader: (filePath) => loadModuleInfoFromFile(filePath, analyzerContext),
          resolveImportedExport: resolveImportedExportCallable,
          analyzeImportedCallable,
        });

        for (const request of bindingAnalysis.requests) {
          const node = createClientRequestNode(
            analyzerContext.serviceName,
            moduleInfo.filePath,
            request,
            descriptor.actionBinding.key
          );
          if (!analyzerContext.state.requestNodeIds.has(node.id)) {
            analyzerContext.state.requestNodeIds.add(node.id);
            analyzerContext.state.nodes.push(node);
          }
          analyzerContext.state.directRequests.push({
            originNodeId: actionId,
            node,
            method: request.method,
            path: request.path,
          });
          analyzerContext.state.edges.push({
            id: `requests:${actionId}:${node.id}`,
            kind: "requests",
            from: actionId,
            to: node.id,
            confidence: request.confidence,
          });
        }

        for (const serverActionRef of bindingAnalysis.serverActionRefs) {
          analyzerContext.state.directServerActionRefs.push({
            originNodeId: actionId,
            ...serverActionRef,
          });
        }
      }

      return descriptor.formContext ?? formContext;
    }

    const componentRef = resolveImportedComponentRef(element, moduleInfo.imports);
    if (componentRef) {
      analyzeImportedComponent(componentRef, analyzerContext);
    }
    return formContext;
  });
}

function analyzeImportedComponent(componentRef, analyzerContext) {
  const moduleInfo = loadModuleInfoFromFile(componentRef.filePath, analyzerContext);
  if (!moduleInfo) return;
  analyzeModuleExport(moduleInfo, componentRef.exportName, analyzerContext);
}

function analyzeImportedCallable(imported, options, visited) {
  if (!imported?.resolvedFilePath || imported.isServerAction || visited.has(imported.resolvedFilePath)) {
    return emptyActionAnalysis();
  }
  const moduleInfo = options.moduleLoader(imported.resolvedFilePath);
  if (!moduleInfo) return emptyActionAnalysis();
  const callable = options.resolveImportedExport(moduleInfo, imported.importedName);
  if (!callable) return emptyActionAnalysis();

  const localFunctions = new Map(moduleInfo.topLevelFunctions);
  for (const [name, node] of collectScopeLocalFunctions(callable)) {
    localFunctions.set(name, node);
  }

  const nextVisited = new Set(visited);
  nextVisited.add(imported.resolvedFilePath);
  return analyzeCallableLikeNode(
    callable,
    {
      imports: moduleInfo.imports,
      localFunctions,
      normalizeRoute: options.normalizeRoute,
      moduleLoader: options.moduleLoader,
      resolveImportedExport: options.resolveImportedExport,
      analyzeImportedCallable,
    },
    nextVisited
  );
}

function loadModuleInfoFromFile(filePath, analyzerContext) {
  const cached = analyzerContext.moduleCache.get(filePath);
  if (cached) return cached;
  const content = analyzerContext.readSourceFile(filePath);
  if (!content) return null;
  return loadModuleInfo(filePath, content, analyzerContext);
}

function loadModuleInfo(filePath, content, analyzerContext) {
  const cached = analyzerContext.moduleCache.get(filePath);
  if (cached) return cached;

  const sourceFile = createSourceFile(filePath, content);
  const imports = collectImports(sourceFile, {
    readSourceFile: analyzerContext.readSourceFile,
    resolveImportPath: (specifier) => analyzerContext.resolveImportPath(filePath, specifier),
    isServerActionFile: analyzerContext.isServerActionFile,
  });
  const topLevelFunctions = collectTopLevelFunctions(sourceFile);
  const exports = collectExportedCallables(sourceFile, topLevelFunctions);
  const moduleInfo = {
    filePath,
    sourceFile,
    imports,
    topLevelFunctions,
    exports,
    defaultExport: findDefaultPageComponent(sourceFile, topLevelFunctions),
  };
  analyzerContext.moduleCache.set(filePath, moduleInfo);
  return moduleInfo;
}

function resolveModuleExportCallable(moduleInfo, exportName) {
  if (!moduleInfo) return null;
  if (exportName === "default") return moduleInfo.defaultExport || null;
  return moduleInfo.exports.get(exportName) || null;
}

function resolveImportedExportCallable(moduleInfo, importedName) {
  if (!moduleInfo) return null;
  if (importedName === "default") return moduleInfo.defaultExport || null;
  return moduleInfo.exports.get(importedName) || null;
}

function resolveImportedComponentRef(node, imports) {
  const opening = ts.isJsxElement(node) ? node.openingElement : node;
  const tagName = opening.tagName.getText();
  if (!/^[A-Z]/u.test(tagName)) return null;
  const imported = imports.get(tagName);
  if (!imported?.resolvedFilePath || imported.isServerAction) return null;
  if (imported.resolvedFilePath.includes("/components/ui/")) return null;
  return {
    filePath: imported.resolvedFilePath,
    exportName: imported.importedName,
  };
}

function resolveGotoRoute(callExpression) {
  const callee = callExpression.expression;
  if (!ts.isPropertyAccessExpression(callee)) return null;
  if (callee.name.text !== "goto") return null;
  const pathArg = callExpression.arguments[0];
  const literal = extractStringLiteral(pathArg);
  if (!literal) return null;
  if (/^https?:\/\//u.test(literal)) {
    try {
      const parsed = new URL(literal);
      if (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1") {
        return defaultNormalizeRoute(parsed.pathname.split("?")[0]);
      }
    } catch { /* ignore malformed */ }
    return null;
  }
  return defaultNormalizeRoute(literal.split("?")[0]);
}

function walkJsx(sourceFile, visitor) {
  const visit = (node, formContext = null) => {
    let nextFormContext = formContext;
    if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
      nextFormContext = visitor(node, formContext);
    }

    if (ts.isJsxElement(node)) {
      for (const child of node.children) {
        visit(child, nextFormContext);
      }
      return;
    }

    ts.forEachChild(node, (child) => visit(child, nextFormContext));
  };

  visit(sourceFile, null);
}

function describeSurface(node, inheritedFormContext) {
  const opening = ts.isJsxElement(node) ? node.openingElement : node;
  const tagName = opening.tagName.getText();
  if (!SURFACE_COMPONENT_NAMES.has(tagName)) return null;

  const attributes = collectJsxAttributes(opening);
  const targetValue = attributes["data-testid"]?.stringValue || null;
  const role = tagName === "form" || tagName === "Form" ? "form" : tagName === "a" || tagName === "Link" ? "link" : "button";
  const surfaceKind =
    role === "form" ? "form" : role === "link" ? "link" : attributes.type?.stringValue === "submit" ? "submit" : "control";

  const ownBinding =
    buildActionBinding(attributes.onClick?.expression, "onClick") ||
    buildActionBinding(attributes.action?.expression, "action") ||
    buildActionBinding(attributes.onSubmit?.expression, "onSubmit");

  const inheritedSubmitBinding =
    !ownBinding && isSubmitSurface(tagName, attributes) && inheritedFormContext?.actionBinding
      ? inheritedFormContext.actionBinding
      : null;

  const actionBinding = ownBinding || inheritedSubmitBinding;
  if (!actionBinding && !targetValue) {
    return tagName === "form" || tagName === "Form"
      ? { formContext: buildFormContext(attributes) }
      : null;
  }

  const label =
    extractSurfaceLabel(node, attributes) ||
    actionBinding?.label ||
    targetValue ||
    humanizeTagName(tagName);
  const line = ts.getLineAndCharacterOfPosition(node.getSourceFile(), node.getStart()).line + 1;
  const target = targetValue
    ? {
        kind: "testId",
        value: targetValue,
        label,
        confidence: "high",
      }
    : null;

  return {
    tagName,
    surfaceKind,
    label,
    line,
    target,
    actionName: actionBinding?.key || null,
    actionBinding,
    formContext: buildFormContext(attributes, actionBinding),
  };
}

function buildFormContext(attributes, explicitBinding = null) {
  const actionBinding =
    explicitBinding ||
    buildActionBinding(attributes.action?.expression, "action") ||
    buildActionBinding(attributes.onSubmit?.expression, "onSubmit");
  return actionBinding ? { actionBinding } : null;
}

function buildActionBinding(expression, actionProp) {
  if (!expression) return null;
  if (ts.isIdentifier(expression)) {
    return {
      kind: "identifier",
      actionProp,
      key: expression.text,
      label: expression.text,
      node: expression,
      confidence: actionProp === "action" ? "high" : "medium",
    };
  }

  if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) {
    const key = `inline:${expression.pos}`;
    return {
      kind: "inline",
      actionProp,
      key,
      label: actionProp === "action" ? "Inline action" : "Inline handler",
      node: expression,
      confidence: "medium",
    };
  }

  return null;
}

function analyzeActionBinding(binding, options) {
  if (!binding) return emptyActionAnalysis();
  if (binding.kind === "identifier" && ts.isIdentifier(binding.node)) {
    const identifier = binding.node.text;
    const imported = options.imports.get(identifier);
    if (imported?.isServerAction) {
      return {
        requests: [],
        serverActionRefs: [
          {
            exportKey: `${imported.resolvedFilePath}#${imported.importedName}`,
            confidence: "high",
          },
        ],
      };
    }

    if (imported?.resolvedFilePath) {
      return options.analyzeImportedCallable(imported, options, new Set([imported.resolvedFilePath]));
    }

    const localFunction = options.localFunctions.get(identifier);
    if (!localFunction) return emptyActionAnalysis();
    return analyzeCallableLikeNode(localFunction, options, new Set([identifier]));
  }

  if (binding.kind === "inline") {
    return analyzeCallableLikeNode(binding.node, options, new Set());
  }

  return emptyActionAnalysis();
}

function analyzeCallableLikeNode(node, options, visited = new Set(), settings = {}) {
  const requests = [];
  const serverActionRefs = [];

  const visit = (child) => {
    if (
      settings.skipNestedFunctions &&
      child !== node &&
      (ts.isArrowFunction(child) || ts.isFunctionExpression(child) || ts.isFunctionDeclaration(child))
    ) {
      return;
    }
    if (ts.isCallExpression(child)) {
      const request = resolveHttpRequestCall(child, options);
      if (request) {
        requests.push(request);
      } else {
        const serverAction = resolveServerActionCall(child, options);
        if (serverAction) {
          serverActionRefs.push(serverAction);
        } else {
          const localCall = resolveLocalFunctionCall(child, options, visited);
          if (localCall) {
            for (const requestEntry of localCall.requests) requests.push(requestEntry);
            for (const serverActionRef of localCall.serverActionRefs) serverActionRefs.push(serverActionRef);
          } else {
            const importedCall = resolveImportedLocalCall(child, options, visited);
            if (importedCall) {
              for (const requestEntry of importedCall.requests) requests.push(requestEntry);
              for (const serverActionRef of importedCall.serverActionRefs) serverActionRefs.push(serverActionRef);
            } else {
              for (const callbackAnalysis of resolveExecutedCallbackAnalyses(child, options, visited)) {
                for (const requestEntry of callbackAnalysis.requests) requests.push(requestEntry);
                for (const serverActionRef of callbackAnalysis.serverActionRefs) serverActionRefs.push(serverActionRef);
              }
            }
          }
        }
      }
    }
    ts.forEachChild(child, visit);
  };

  if (ts.isSourceFile(node)) {
    ts.forEachChild(node, visit);
  } else if ("body" in node && node.body) {
    visit(node.body);
  } else if (ts.isBlock(node) || ts.isExpression(node)) {
    visit(node);
  }

  return {
    requests: dedupeRequests(requests),
    serverActionRefs: dedupeServerActionRefs(serverActionRefs),
  };
}

function resolveLocalFunctionCall(callExpression, options, visited) {
  const callee = callExpression.expression;
  if (!ts.isIdentifier(callee)) return null;
  const functionName = callee.text;
  const localFunction = options.localFunctions.get(functionName);
  if (!localFunction || visited.has(functionName)) return null;
  const nextVisited = new Set(visited);
  nextVisited.add(functionName);
  return analyzeCallableLikeNode(localFunction, options, nextVisited);
}

function resolveImportedLocalCall(callExpression, options, visited) {
  const callee = callExpression.expression;
  if (!ts.isIdentifier(callee)) return null;
  const imported = options.imports.get(callee.text);
  if (!imported?.resolvedFilePath || imported.isServerAction) return null;
  return options.analyzeImportedCallable(imported, options, visited);
}

function resolveExecutedCallbackAnalyses(callExpression, options, visited) {
  const callbacks = [];
  const callee = callExpression.expression;
  if (ts.isIdentifier(callee) && (callee.text === "useEffect" || callee.text === "useLayoutEffect")) {
    const callbackArg = callExpression.arguments[0];
    if (callbackArg && (ts.isArrowFunction(callbackArg) || ts.isFunctionExpression(callbackArg))) {
      callbacks.push(analyzeCallableLikeNode(callbackArg, options, new Set(visited)));
    }
  }
  return callbacks;
}

function resolveServerActionCall(callExpression, options) {
  const callee = callExpression.expression;
  if (!ts.isIdentifier(callee)) return null;
  const imported = options.imports.get(callee.text);
  if (!imported?.isServerAction) return null;
  return {
    exportKey: `${imported.resolvedFilePath}#${imported.importedName}`,
    confidence: "high",
  };
}

function resolveHttpRequestCall(callExpression, options) {
  const callee = callExpression.expression;
  if (ts.isIdentifier(callee)) {
    if (callee.text === "fetch") {
      const path = extractRouteLiteral(callExpression.arguments[0], options.normalizeRoute);
      if (!path || !path.startsWith("/api/")) return null;
      return {
        method: extractFetchMethod(callExpression.arguments[1]) || "GET",
        path,
        confidence: "high",
      };
    }

    const method = HTTP_WRAPPER_METHODS[callee.text];
    if (method) {
      const path = extractRouteLiteral(callExpression.arguments[0], options.normalizeRoute);
      if (!path || !path.startsWith("/api/")) return null;
      return {
        method,
        path,
        confidence: "high",
      };
    }

    if (callee.text === "rawReq") {
      const methodLiteral = extractStringLiteral(callExpression.arguments[0]);
      const path = extractRouteLiteral(callExpression.arguments[1], options.normalizeRoute);
      if (!methodLiteral || !path) return null;
      return {
        method: methodLiteral.toUpperCase(),
        path,
        confidence: "high",
      };
    }
  }

  if (ts.isPropertyAccessExpression(callee) && callee.name.text === "rawReq") {
    const methodLiteral = extractStringLiteral(callExpression.arguments[0]);
    const path = extractRouteLiteral(callExpression.arguments[1], options.normalizeRoute);
    if (!methodLiteral || !path) return null;
    return {
      method: methodLiteral.toUpperCase(),
      path,
      confidence: "high",
    };
  }

  return null;
}

function collectImports(sourceFile, options) {
  const imports = new Map();
  for (const statement of sourceFile.statements) {
    if (!ts.isImportDeclaration(statement) || !statement.importClause) continue;
    const specifier = extractStringLiteral(statement.moduleSpecifier);
    if (!specifier) continue;
    const resolvedFilePath = options.resolveImportPath(specifier);
    const sourceContent = resolvedFilePath ? options.readSourceFile(resolvedFilePath) : null;
    const serverActionImport = Boolean(sourceContent && options.isServerActionFile(sourceContent));

    if (statement.importClause.name) {
      imports.set(statement.importClause.name.text, {
        importedName: "default",
        specifier,
        resolvedFilePath,
        isServerAction: serverActionImport,
      });
    }

    const bindings = statement.importClause.namedBindings;
    if (!bindings || !ts.isNamedImports(bindings)) continue;
    for (const element of bindings.elements) {
      const localName = element.name.text;
      const importedName = element.propertyName?.text || localName;
      imports.set(localName, {
        importedName,
        specifier,
        resolvedFilePath,
        isServerAction: serverActionImport,
      });
    }
  }
  return imports;
}

function collectTopLevelFunctions(sourceFile) {
  const functions = new Map();
  for (const statement of sourceFile.statements) {
    if (ts.isFunctionDeclaration(statement) && statement.name) {
      functions.set(statement.name.text, statement);
      continue;
    }

    if (!ts.isVariableStatement(statement)) continue;
    for (const declaration of statement.declarationList.declarations) {
      if (!ts.isIdentifier(declaration.name) || !declaration.initializer) continue;
      if (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer)) {
        functions.set(declaration.name.text, declaration.initializer);
      }
    }
  }
  return functions;
}

function collectExportedCallables(sourceFile, topLevelFunctions) {
  const exports = new Map();
  for (const statement of sourceFile.statements) {
    if (ts.isFunctionDeclaration(statement) && statement.name && hasExportModifier(statement.modifiers)) {
      exports.set(statement.name.text, statement);
      continue;
    }

    if (!ts.isVariableStatement(statement) || !hasExportModifier(statement.modifiers)) continue;
    for (const declaration of statement.declarationList.declarations) {
      if (!ts.isIdentifier(declaration.name) || !declaration.initializer) continue;
      if (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer)) {
        exports.set(declaration.name.text, declaration.initializer);
      } else if (ts.isIdentifier(declaration.initializer)) {
        const referenced = topLevelFunctions.get(declaration.initializer.text);
        if (referenced) exports.set(declaration.name.text, referenced);
      }
    }
  }
  return exports;
}

function collectScopeLocalFunctions(node) {
  const functions = new Map();
  const callableBody = "body" in node ? node.body : null;
  if (!callableBody) return functions;

  const visit = (child) => {
    if (
      child !== callableBody &&
      (ts.isFunctionDeclaration(child) || ts.isArrowFunction(child) || ts.isFunctionExpression(child))
    ) {
      return;
    }

    if (ts.isFunctionDeclaration(child) && child.name) {
      functions.set(child.name.text, child);
    }

    if (ts.isVariableDeclaration(child) && ts.isIdentifier(child.name) && child.initializer) {
      if (ts.isArrowFunction(child.initializer) || ts.isFunctionExpression(child.initializer)) {
        functions.set(child.name.text, child.initializer);
      }
    }

    ts.forEachChild(child, visit);
  };

  visit(callableBody);
  return functions;
}

function findDefaultPageComponent(sourceFile, localFunctions) {
  for (const statement of sourceFile.statements) {
    if (ts.isFunctionDeclaration(statement) && hasDefaultModifier(statement.modifiers)) {
      return statement;
    }

    if (ts.isExportAssignment(statement)) {
      if (ts.isIdentifier(statement.expression)) {
        return localFunctions.get(statement.expression.text) || null;
      }
      if (ts.isArrowFunction(statement.expression) || ts.isFunctionExpression(statement.expression)) {
        return statement.expression;
      }
    }
  }
  return null;
}

function hasExportModifier(modifiers) {
  return Boolean(modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword));
}

function collectJsxAttributes(openingElement) {
  const attributes = {};
  for (const property of openingElement.attributes.properties) {
    if (!ts.isJsxAttribute(property)) continue;
    const name = property.name.text;
    if (!property.initializer) {
      attributes[name] = { stringValue: "true", expression: null };
      continue;
    }
    if (ts.isStringLiteral(property.initializer)) {
      attributes[name] = { stringValue: property.initializer.text, expression: null };
      continue;
    }
    if (ts.isJsxExpression(property.initializer)) {
      attributes[name] = {
        stringValue: extractStringLiteral(property.initializer.expression),
        expression: property.initializer.expression || null,
      };
    }
  }
  return attributes;
}

function extractSurfaceLabel(node, attributes) {
  const ariaLabel = attributes["aria-label"]?.stringValue || attributes.title?.stringValue || null;
  if (ariaLabel) return ariaLabel;

  if (ts.isJsxElement(node)) {
    const text = flattenJsxText(node.children).trim();
    if (text) return text;
  }

  return null;
}

function flattenJsxText(children) {
  let text = "";
  for (const child of children) {
    if (ts.isJsxText(child)) {
      text += child.getText().replace(/\s+/gu, " ");
    } else if (ts.isJsxExpression(child) && child.expression && ts.isStringLiteralLike(child.expression)) {
      text += child.expression.text;
    } else if (ts.isJsxElement(child)) {
      text += flattenJsxText(child.children);
    }
  }
  return text;
}

function extractRouteLiteral(node, normalizeRoute) {
  const literal = extractStringLiteral(node);
  if (!literal) return null;
  return normalizeRoute(literal.split("?")[0]);
}

function extractFetchMethod(node) {
  if (!node || !ts.isObjectLiteralExpression(node)) return null;
  for (const property of node.properties) {
    if (!ts.isPropertyAssignment(property)) continue;
    if (property.name.getText() !== "method") continue;
    const value = extractStringLiteral(property.initializer);
    return value ? value.toUpperCase() : null;
  }
  return null;
}

function extractStringLiteral(node) {
  if (!node) return null;
  if (ts.isStringLiteralLike(node)) return node.text;
  if (ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
  if (ts.isTemplateExpression(node)) {
    let value = node.head.text;
    for (const span of node.templateSpans) {
      value += DYNAMIC_SEGMENT_TOKEN;
      value += span.literal.text;
    }
    return value;
  }
  return null;
}

function createClientRequestNode(serviceName, filePath, request, ownerKey) {
  return {
    id: `client_request:${serviceName}:${filePath}:${ownerKey}:${request.method}:${request.path}`,
    kind: "client_request",
    service: serviceName,
    label: `${request.method} ${request.path}`,
    method: request.method,
    path: request.path,
    filePath,
  };
}

function createSourceFile(filePath, content) {
  const scriptKind = filePath.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
  return ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, scriptKind);
}

function hasDefaultModifier(modifiers) {
  return Boolean(modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword));
}

function defaultNormalizeRoute(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 isSubmitSurface(tagName, attributes) {
  const type = attributes.type?.stringValue || "";
  return tagName === "button" || tagName === "Button" || (tagName === "input" && type === "submit");
}

function humanizeTagName(tagName) {
  return tagName.replace(/([a-z])([A-Z])/gu, "$1 $2").replace(/^\w/u, (char) => char.toUpperCase());
}

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

function dedupeById(entries) {
  const seen = new Set();
  return entries.filter((entry) => {
    if (seen.has(entry.id)) return false;
    seen.add(entry.id);
    return true;
  });
}

function dedupeServerActionRefs(entries) {
  const seen = new Set();
  return entries.filter((entry) => {
    const key = `${entry.originNodeId || ""}:${entry.exportKey}`;
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
}

function emptyActionAnalysis() {
  return {
    requests: [],
    serverActionRefs: [],
  };
}

export function resolveImportToSourceFile(serviceRoot, fromFilePath, specifier) {
  const candidates = [];
  const sourceRoot = pathExists(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 (fileExists(candidate)) 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 (fileExists(candidate)) return candidate;
  }
  return null;
}

function pathExists(value) {
  try {
    return Boolean(ts.sys.fileExists(value) || ts.sys.directoryExists(value));
  } catch {
    return false;
  }
}

function fileExists(value) {
  try {
    return Boolean(ts.sys.fileExists(value));
  } catch {
    return false;
  }
}

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