import { resolve, dirname, join } from "node:path";
import type { ImportRef } from "../types/index.js";
import type { FileStore } from "../storage/file-store.js";
import type { GraphStore } from "../storage/graph-store.js";
import { computePageRank } from "../search/pagerank.js";
import { loadSverkloConfig, getWeight } from "../utils/config-file.js";
import { log } from "../utils/logger.js";

// Default PageRank weight for non-code files. Without this, repos with
// many top-level docs (BENCHMARKS.md, FIRST_RUN.md, README.md, etc.) end
// up with markdown ranked above source — sverklo audit then claims a
// no-deps repo deserves an A. User config can override via `weights:`
// in `.sverklo.yaml`.
const NON_CODE_WEIGHT = 0.1;
const NON_CODE_PATTERNS = [
  /\.md$/i,
  /\.markdown$/i,
  /\.mdx$/i,
  /\.ya?ml$/i,
  /\.json$/i,
  /\.toml$/i,
  /\.lock$/i,
  /\.txt$/i,
  /\.cfg$/i,
  /\.ini$/i,
  /\.csv$/i,
  /\.tsv$/i,
];

function defaultFileWeight(path: string): number {
  for (const re of NON_CODE_PATTERNS) {
    if (re.test(path)) return NON_CODE_WEIGHT;
  }
  return 1.0;
}

// File extension resolution for relative imports
const EXTENSIONS = [
  "",
  ".ts",
  ".tsx",
  ".js",
  ".jsx",
  ".mts",
  ".cts",
  ".mjs",
  ".cjs",
  "/index.ts",
  "/index.tsx",
  "/index.js",
  "/index.jsx",
  ".py",
  ".go",
  ".rs",
  ".java",
  ".rb",
  ".php",
];

export function buildGraph(
  fileImports: Map<string, ImportRef[]>, // relativePath -> imports
  fileStore: FileStore,
  graphStore: GraphStore,
  rootPath: string
): void {
  const allFiles = fileStore.getAll();
  const pathToId = new Map<string, number>();
  for (const f of allFiles) {
    pathToId.set(f.path, f.id);
  }

  // Clear outgoing edges for every file that was just re-parsed. The
  // incremental reindexFile() path already does this before calling
  // buildGraph (indexer.ts:451), but the full-index path didn't —
  // upsert alone doesn't remove edges that no longer exist in the
  // re-parsed import list. Symptom: a consumer that switches from
  // `import type { Indexer }` to `import type { IndexFiles }` keeps
  // its old edge to indexer.ts AND adds a new edge to index-files.ts,
  // doubling fan-in instead of moving it. Discovered while running
  // the indexer-coupling refactor POC.
  for (const filePath of fileImports.keys()) {
    const sourceId = pathToId.get(filePath);
    if (sourceId !== undefined) {
      graphStore.deleteBySourceFile(sourceId);
    }
  }

  // Build edges
  const edges: { source: number; target: number }[] = [];

  for (const [filePath, imports] of fileImports) {
    const sourceId = pathToId.get(filePath);
    if (!sourceId) continue;

    const fileDir = dirname(filePath);

    for (const imp of imports) {
      if (!imp.isRelative) continue;

      // Try to resolve the import to a file in the index
      const resolved = resolveImport(imp.source, fileDir, pathToId);
      if (resolved !== undefined) {
        graphStore.upsert(sourceId, resolved, imp.names.length || 1);
        edges.push({ source: sourceId, target: resolved });
      }
    }
  }

  // Compute PageRank
  const fileIds = allFiles.map((f) => f.id);
  const ranks = computePageRank(fileIds, edges);
  const config = loadSverkloConfig(rootPath);

  // Build reverse map for weight lookups
  const idToPath = new Map<number, string>();
  for (const [path, id] of pathToId) {
    idToPath.set(id, path);
  }

  // Write ranks back to file store. Non-code files get a built-in 0.1×
  // demotion (kills markdown-dominated topFiles) UNLESS the user config
  // explicitly weights them — user config wins.
  for (const [id, rank] of ranks) {
    const filePath = idToPath.get(id);
    const userWeight = filePath ? getWeight(config, filePath) : 1.0;
    const builtinWeight =
      filePath && userWeight === 1.0 ? defaultFileWeight(filePath) : 1.0;
    fileStore.updatePagerank(id, rank * userWeight * builtinWeight);
  }

  log(`Graph built: ${edges.length} edges, PageRank computed for ${fileIds.length} files`);
}

function resolveImport(
  importPath: string,
  fromDir: string,
  pathToId: Map<string, number>
): number | undefined {
  const resolved = join(fromDir, importPath);

  // Strip JS-family extensions so we can re-resolve to .ts, .tsx, etc.
  const stripped = resolved.replace(/\.(m?jsx?|cjs)$/, "");
  const bases = stripped !== resolved ? [stripped, resolved] : [resolved];

  for (const base of bases) {
    for (const ext of EXTENSIONS) {
      const candidate = base + ext;
      // Normalize path separators
      const normalized = candidate.replace(/\\/g, "/");
      const id = pathToId.get(normalized);
      if (id !== undefined) return id;
    }
  }

  return undefined;
}
