import { isGlobPattern } from "./fuzzy-matcher.js";
import picomatch from "picomatch";
const DATA_BEARING_EDGES = [
  "call",
  "instantiation",
  "param_type",
  "return_type",
  "relation",
  "dispatch",
  "notify",
  "route",
  "render"
];
const STRUCTURAL_EDGES = [
  "import",
  "require",
  "extends",
  "implements",
  "binding",
  "middleware"
];
function matchesSymbol(nodeSymbol, edgeSymbol) {
  if (!nodeSymbol) return true;
  if (!edgeSymbol) return true;
  if (nodeSymbol === edgeSymbol) return true;
  const normNode = nodeSymbol.replace(/\./g, "::").toLowerCase();
  const normEdge = edgeSymbol.replace(/\./g, "::").toLowerCase();
  if (normNode === normEdge) return true;
  if (nodeSymbol.includes("::")) {
    const parts = nodeSymbol.split("::");
    const name = parts[parts.length - 1];
    if (edgeSymbol.toLowerCase() === name.toLowerCase()) return true;
  }
  if (nodeSymbol.includes(".")) {
    const parts = nodeSymbol.split(".");
    const last = parts[parts.length - 1].toLowerCase();
    const commonExtensions = /* @__PURE__ */ new Set([
      "ts",
      "js",
      "cs",
      "java",
      "py",
      "php",
      "go",
      "rs",
      "swift",
      "kt",
      "svelte",
      "vue",
      "lua",
      "ex",
      "zig",
      "sh",
      "pas",
      "scala",
      "rb",
      "c",
      "cpp",
      "h",
      "hpp",
      "json",
      "yaml",
      "yml",
      "md"
    ]);
    if (!commonExtensions.has(last)) {
      const name = parts[parts.length - 1];
      if (edgeSymbol.toLowerCase() === name.toLowerCase()) return true;
    }
  }
  return false;
}
class FlowTracer {
  constructor(store) {
    this.store = store;
  }
  store;
  trace(options) {
    const repo = options.repo;
    const direction = options.direction;
    const maxDepth = options.maxDepth ?? 3;
    let startFile = options.startFile;
    let startSymbol = options.startSymbol;
    if (!startFile && startSymbol) {
      const resolved = this.resolveStart(startSymbol, repo);
      if (resolved) {
        startFile = resolved.file;
        startSymbol = resolved.symbol ?? void 0;
      }
    } else if (startFile && !startSymbol) {
      const resolved = this.resolveStart(startFile, repo);
      if (resolved) {
        startFile = resolved.file;
        startSymbol = resolved.symbol ?? void 0;
      }
    }
    if (!startFile) {
      throw new Error(`Could not resolve starting file or symbol: ${options.startFile || options.startSymbol}`);
    }
    const startNode = {
      file: startFile,
      symbol: startSymbol || null,
      depth: 0,
      incomingEdgeType: "start"
    };
    if (direction === "both") {
      const downRes = this.trace({ ...options, direction: "down", startFile, startSymbol });
      const upRes = this.trace({ ...options, direction: "up", startFile, startSymbol });
      const uniqueNodesMap = /* @__PURE__ */ new Map();
      const addNode = (n) => {
        const key = `${n.file}::${n.symbol || ""}`;
        if (!uniqueNodesMap.has(key) || uniqueNodesMap.get(key).depth > n.depth) {
          uniqueNodesMap.set(key, n);
        }
      };
      for (const p of [...downRes.paths, ...upRes.paths]) {
        for (const n of p.nodes) addNode(n);
      }
      const mergedCycles = [...downRes.cycles, ...upRes.cycles];
      const mergedSources = [...downRes.sources, ...upRes.sources];
      const mergedSinks = [...downRes.sinks, ...upRes.sinks];
      return {
        start: { file: startFile, symbol: startSymbol || null },
        direction: "both",
        paths: [...downRes.paths, ...upRes.paths],
        sources: Array.from(new Map(mergedSources.map((n) => [`${n.file}::${n.symbol || ""}`, n])).values()),
        sinks: Array.from(new Map(mergedSinks.map((n) => [`${n.file}::${n.symbol || ""}`, n])).values()),
        cycles: Array.from(new Map(mergedCycles.map((c) => [`${c.fromFile}->${c.toFile}`, c])).values()),
        nodeCount: uniqueNodesMap.size,
        edgeCount: downRes.edgeCount + upRes.edgeCount,
        maxDepthReached: downRes.maxDepthReached || upRes.maxDepthReached
      };
    }
    const allPaths = [];
    const detectedCycles = [];
    const stack = [startNode];
    const stackKeys = /* @__PURE__ */ new Set([`${startFile}::${startSymbol || ""}`]);
    const uniqueVisited = /* @__PURE__ */ new Set([`${startFile}::${startSymbol || ""}`]);
    let maxDepthReached = false;
    const getPathKey = (file, symbol) => `${file}::${symbol || ""}`;
    const dfs = (current) => {
      const edges = this.getEdgesForNode(current, direction, options.includeStructural, repo);
      let hasValidSteps = false;
      const validNextEdges = [];
      for (const edge of edges) {
        const nextFile = direction === "up" ? edge.source_file : edge.target_file;
        const nextSymbolName = direction === "up" ? edge.source_symbol : edge.target_symbol;
        const nextSymbol = this.getScopedSymbolName(nextFile, nextSymbolName, repo);
        const nextKey = getPathKey(nextFile, nextSymbol);
        if (stackKeys.has(nextKey)) {
          const ancestorIndex = stack.findIndex((n) => getPathKey(n.file, n.symbol) === nextKey);
          const cycleLength = stack.length - ancestorIndex;
          detectedCycles.push({
            fromFile: current.file,
            fromSymbol: current.symbol,
            toFile: nextFile,
            toSymbol: nextSymbol,
            edgeType: edge.edge_type,
            cycleLength
          });
        } else {
          hasValidSteps = true;
          validNextEdges.push({ edge, nextFile, nextSymbol, nextKey });
        }
      }
      if (!hasValidSteps || current.depth >= maxDepth) {
        allPaths.push({
          nodes: [...stack],
          cycles: []
        });
        if (current.depth >= maxDepth && hasValidSteps) {
          maxDepthReached = true;
        }
        return;
      }
      for (const next of validNextEdges) {
        if (uniqueVisited.has(next.nextKey)) {
          continue;
        }
        const nextNode = {
          file: next.nextFile,
          symbol: next.nextSymbol,
          depth: current.depth + 1,
          incomingEdgeType: next.edge.edge_type
        };
        stack.push(nextNode);
        stackKeys.add(next.nextKey);
        uniqueVisited.add(next.nextKey);
        dfs(nextNode);
        stack.pop();
        stackKeys.delete(next.nextKey);
      }
    };
    dfs(startNode);
    const uniqueNodes = /* @__PURE__ */ new Set();
    const traceEdges = /* @__PURE__ */ new Set();
    for (const p of allPaths) {
      for (let i = 0; i < p.nodes.length; i++) {
        uniqueNodes.add(`${p.nodes[i].file}::${p.nodes[i].symbol || ""}`);
        if (i > 0) {
          const from = direction === "up" ? p.nodes[i].file : p.nodes[i - 1].file;
          const to = direction === "up" ? p.nodes[i - 1].file : p.nodes[i].file;
          traceEdges.add(`${from}->${to}`);
        }
      }
    }
    const traceSources = [];
    const traceSinks = [];
    for (const key of uniqueNodes) {
      const firstColon = key.indexOf("::");
      const file = firstColon !== -1 ? key.substring(0, firstColon) : key;
      const symbol = firstColon !== -1 ? key.substring(firstColon + 2) : "";
      const nodeSym = symbol || null;
      const node = { file, symbol: nodeSym, depth: 0, incomingEdgeType: "start" };
      const outEdges = this.getEdgesForNode(node, "down", options.includeStructural, repo);
      const inEdges = this.getEdgesForNode(node, "up", options.includeStructural, repo);
      if (inEdges.length === 0) {
        traceSources.push(node);
      }
      if (outEdges.length === 0) {
        traceSinks.push(node);
      }
    }
    return {
      start: { file: startFile, symbol: startSymbol || null },
      direction,
      paths: allPaths,
      sources: traceSources,
      sinks: traceSinks,
      cycles: detectedCycles,
      nodeCount: uniqueNodes.size,
      edgeCount: traceEdges.size,
      maxDepthReached
    };
  }
  resolveStart(input, repo) {
    const allFiles = this.store.getAllFiles(repo);
    const paths = allFiles.map((f) => f.path);
    let matchedPath = paths.find((p) => p === input);
    if (!matchedPath && isGlobPattern(input)) {
      const matchBase = !input.includes("/");
      const isMatch = picomatch(input, { dot: true, nocase: true, matchBase });
      matchedPath = paths.find((p) => isMatch(p));
    }
    if (!matchedPath) {
      matchedPath = paths.find((p) => p.endsWith(input) || p.includes(input));
    }
    if (matchedPath) {
      return { file: matchedPath, symbol: null };
    }
    let scope = null;
    let name = input;
    if (input.includes("::")) {
      const parts = input.split("::");
      name = parts.pop();
      scope = parts.join("::");
    } else if (input.includes(".")) {
      const parts = input.split(".");
      const last = parts[parts.length - 1].toLowerCase();
      const commonExtensions = /* @__PURE__ */ new Set([
        "ts",
        "js",
        "cs",
        "java",
        "py",
        "php",
        "go",
        "rs",
        "swift",
        "kt",
        "svelte",
        "vue",
        "lua",
        "ex",
        "zig",
        "sh",
        "pas",
        "scala",
        "rb",
        "c",
        "cpp",
        "h",
        "hpp",
        "json",
        "yaml",
        "yml",
        "md"
      ]);
      if (!commonExtensions.has(last)) {
        name = parts.pop();
        scope = parts.join(".");
      }
    }
    const allSymbols = this.store.getAllSymbols(repo);
    let matchedSymbol = null;
    if (scope) {
      matchedSymbol = allSymbols.find((s) => s.name === name && s.scope === scope);
      if (!matchedSymbol) {
        const normScope = scope.replace(/\./g, "::").toLowerCase();
        matchedSymbol = allSymbols.find((s) => s.name === name && s.scope && s.scope.replace(/\./g, "::").toLowerCase() === normScope);
      }
    } else {
      matchedSymbol = allSymbols.find((s) => s.name === name || s.scope === name);
    }
    if (matchedSymbol) {
      return {
        file: matchedSymbol.file_path,
        symbol: matchedSymbol.scope ? `${matchedSymbol.scope}::${matchedSymbol.name}` : matchedSymbol.name
      };
    }
    return null;
  }
  findSources(repo) {
    const files = this.store.getAllFiles(repo);
    const edges = this.store.getAllEdges(repo);
    const filePaths = new Set(files.map((f) => f.path));
    for (const e of edges) {
      filePaths.add(e.source_file);
      filePaths.add(e.target_file);
    }
    const dataEdges = edges.filter((e) => DATA_BEARING_EDGES.includes(e.edge_type));
    const incomingCount = /* @__PURE__ */ new Map();
    const outgoingCount = /* @__PURE__ */ new Map();
    for (const p of filePaths) {
      incomingCount.set(p, 0);
      outgoingCount.set(p, 0);
    }
    for (const e of dataEdges) {
      const src = e.source_file;
      const tgt = e.target_file;
      outgoingCount.set(src, (outgoingCount.get(src) || 0) + 1);
      incomingCount.set(tgt, (incomingCount.get(tgt) || 0) + 1);
    }
    const sources = [];
    for (const p of filePaths) {
      const inc = incomingCount.get(p) || 0;
      const out = outgoingCount.get(p) || 0;
      if (inc === 0 && out > 0) {
        sources.push({
          file: p,
          symbol: null,
          depth: 0,
          incomingEdgeType: "start"
        });
      }
    }
    return sources;
  }
  findSinks(repo) {
    const files = this.store.getAllFiles(repo);
    const edges = this.store.getAllEdges(repo);
    const filePaths = new Set(files.map((f) => f.path));
    for (const e of edges) {
      filePaths.add(e.source_file);
      filePaths.add(e.target_file);
    }
    const dataEdges = edges.filter((e) => DATA_BEARING_EDGES.includes(e.edge_type));
    const incomingCount = /* @__PURE__ */ new Map();
    const outgoingCount = /* @__PURE__ */ new Map();
    for (const p of filePaths) {
      incomingCount.set(p, 0);
      outgoingCount.set(p, 0);
    }
    for (const e of dataEdges) {
      const src = e.source_file;
      const tgt = e.target_file;
      outgoingCount.set(src, (outgoingCount.get(src) || 0) + 1);
      incomingCount.set(tgt, (incomingCount.get(tgt) || 0) + 1);
    }
    const sinks = [];
    for (const p of filePaths) {
      const inc = incomingCount.get(p) || 0;
      const out = outgoingCount.get(p) || 0;
      if (out === 0 && inc > 0) {
        sinks.push({
          file: p,
          symbol: null,
          depth: 0,
          incomingEdgeType: "start"
        });
      }
    }
    return sinks;
  }
  findCriticalPath(from, to, repo) {
    const startNode = this.resolveStart(from, repo);
    if (!startNode) return null;
    const targetNode = this.resolveStart(to, repo);
    if (!targetNode) return null;
    const getPathKey = (file, symbol) => `${file}::${symbol || ""}`;
    const targetKey = getPathKey(targetNode.file, targetNode.symbol);
    const queue = [
      { node: { file: startNode.file, symbol: startNode.symbol, depth: 0, incomingEdgeType: "start" }, path: [{ file: startNode.file, symbol: startNode.symbol, depth: 0, incomingEdgeType: "start" }] }
    ];
    const visited = /* @__PURE__ */ new Set([getPathKey(startNode.file, startNode.symbol)]);
    while (queue.length > 0) {
      const { node, path } = queue.shift();
      const currentKey = getPathKey(node.file, node.symbol);
      if (currentKey === targetKey) {
        return {
          nodes: path,
          cycles: []
        };
      }
      const edges = this.getEdgesForNode(node, "down", false, repo);
      for (const edge of edges) {
        const nextFile = edge.target_file;
        const nextSymbolName = edge.target_symbol;
        const nextSymbol = this.getScopedSymbolName(nextFile, nextSymbolName, repo);
        const nextKey = getPathKey(nextFile, nextSymbol);
        if (!visited.has(nextKey)) {
          visited.add(nextKey);
          const nextNode = {
            file: nextFile,
            symbol: nextSymbol,
            depth: node.depth + 1,
            incomingEdgeType: edge.edge_type
          };
          queue.push({
            node: nextNode,
            path: [...path, nextNode]
          });
        }
      }
    }
    return null;
  }
  getEdgesForNode(node, direction, includeStructural, repo) {
    const edges = direction === "up" ? this.store.getReverseEdges(node.file) : this.store.getEdgesForFile(node.file);
    let filtered = edges;
    if (repo) {
      filtered = filtered.filter((e) => e.repo === repo);
    }
    const isDataBearing = (type) => DATA_BEARING_EDGES.includes(type);
    const isStructural = (type) => STRUCTURAL_EDGES.includes(type);
    filtered = filtered.filter((e) => {
      const type = e.edge_type;
      if (includeStructural) {
        return isDataBearing(type) || isStructural(type);
      }
      return isDataBearing(type);
    });
    filtered = filtered.filter((e) => {
      const edgeSym = direction === "up" ? e.target_symbol : e.source_symbol;
      return matchesSymbol(node.symbol, edgeSym);
    });
    return filtered;
  }
  getScopedSymbolName(file, name, repo) {
    if (!name) return null;
    const symbols = this.store.getSymbolsForFile(file);
    const found = symbols.find((s) => s.name === name);
    if (found && found.scope) {
      return `${found.scope}::${name}`;
    }
    return name;
  }
}
export {
  FlowTracer
};
