import fs from 'node:fs/promises';
import * as path from 'path';
import { ensureCrGraphBuilt } from '../../engines/code-review-graph-client.js';
const FILE_LIMIT = 120;
const MEMORY_LIMIT = 60;
const FILE_READ_LIMIT = 64 * 1024;
const FALLBACK_SCAN_DIR_LIMIT = 420;
const MAX_STRUCTURAL_EDGES = 360;
const MAX_MODULE_EDGES = 300;
const MAX_MEMORY_OVERLAY_EDGES = FILE_LIMIT * 2;
const MAX_MEMORY_ASSOC_EDGES = 120;
const MAX_CONCEPT_EDGES = 240;
const MEMORY_FILE_SIM_THRESHOLD = 0.08;
const MEMORY_ASSOC_SIM_THRESHOLD = 0.12;
const CONCEPT_MIN_OCCURRENCES = 2;
const CONCEPT_MAX = 20;
const IMPORT_CANDIDATE_SUFFIXES = [
    '',
    '.ts', '.tsx', '.js', '.jsx', '.mts', '.cts',
    '.json', '.yaml', '.yml', '.toml',
    `${path.sep}index.ts`, `${path.sep}index.tsx`,
    `${path.sep}index.js`, `${path.sep}index.jsx`,
];
const FALLBACK_SCAN_IGNORE = new Set([
    '.git', '.next', '.turbo', '.cache', '.nexus', '.nexus-prime',
    'node_modules', 'dist', 'build', 'coverage', 'vendor',
]);
const FALLBACK_FILE_PATTERN = /\.(ts|tsx|js|jsx|mts|cts|json|yaml|yml|toml|md|mdx|css)$/i;
const GENERIC_TERMS = new Set([
    'the', 'and', 'for', 'with', 'this', 'that', 'are', 'was',
    'has', 'from', 'not', 'but', 'all', 'use', 'its', 'via',
    'can', 'will', 'when', 'into', 'also', 'only', 'each',
    'been', 'have', 'more', 'than', 'some', 'any', 'how',
]);
const GENERIC_TAGS = new Set([
    'task', 'finding', 'note', 'important', 'todo', 'fix', 'done',
    'bug', 'feature', 'info', 'update', 'change', 'work', 'new',
    'code', 'file', 'data', 'type', 'value', 'item', 'list',
    'run', 'log', 'error', 'result', 'output', 'input',
]);
// ── File helpers ─────────────────────────────────────────────────────────────
function classifyFileKind(relPath) {
    const normalized = relPath.replace(/\\/g, '/');
    const ext = path.extname(normalized).toLowerCase();
    if (/(^|\/)(__tests__|tests)\//.test(normalized) || /\.(test|spec)\.[^.]+$/.test(normalized)) {
        return 'test';
    }
    if (/\.(ts|tsx|js|jsx|mts|cts)$/.test(ext)) {
        return 'source';
    }
    if (/\.(json|yaml|yml|toml)$/.test(ext) || /(^|\/)(tsconfig|vite|vitest|jest|eslint|prettier|rollup|webpack|babel|playwright|cypress|package-lock|pnpm-lock|yarn)\b/.test(normalized)) {
        return 'config';
    }
    return 'other';
}
function extractModulePath(relPath) {
    const normalized = relPath.replace(/\\/g, '/');
    const parts = normalized.split('/');
    if (parts.length < 2)
        return null;
    if (parts[0] === 'src' && parts.length >= 3)
        return `${parts[0]}/${parts[1]}`;
    if (['test', 'tests', 'scripts'].includes(parts[0]))
        return parts[0];
    return parts[0];
}
async function readFileSnippet(absPath) {
    try {
        const stat = await fs.stat(absPath);
        if (!stat.isFile() || stat.size > FILE_READ_LIMIT)
            return '';
        return await fs.readFile(absPath, 'utf8');
    }
    catch {
        return '';
    }
}
async function scanTopologyFilesFallback(repoRoot, limit) {
    const root = path.resolve(repoRoot);
    const files = [];
    const queue = [root];
    let dirsVisited = 0;
    while (queue.length && files.length < limit && dirsVisited < FALLBACK_SCAN_DIR_LIMIT) {
        const dir = queue.shift();
        dirsVisited++;
        let entries = [];
        try {
            entries = await fs.readdir(dir, { withFileTypes: true });
        }
        catch {
            continue;
        }
        entries.sort((a, b) => {
            if (a.isDirectory() !== b.isDirectory())
                return a.isDirectory() ? -1 : 1;
            return a.name.localeCompare(b.name);
        });
        for (const entry of entries) {
            if (files.length >= limit)
                break;
            if (FALLBACK_SCAN_IGNORE.has(entry.name))
                continue;
            const absPath = path.join(dir, entry.name);
            if (entry.isDirectory()) {
                queue.push(absPath);
                continue;
            }
            if (!entry.isFile() || !FALLBACK_FILE_PATTERN.test(entry.name))
                continue;
            files.push(absPath);
        }
    }
    return files;
}
function extractRelativeImports(source) {
    const specs = new Set();
    const pattern = /\bimport\s+(?:[^'"]+from\s+)?['"]([^'"]+)['"]|\brequire\(\s*['"]([^'"]+)['"]\s*\)|\bimport\(\s*['"]([^'"]+)['"]\s*\)/g;
    let match = null;
    while ((match = pattern.exec(source))) {
        const spec = String(match[1] || match[2] || match[3] || '').trim();
        if (spec.startsWith('.'))
            specs.add(spec);
    }
    return [...specs];
}
function resolveRelativeTarget(importerAbsPath, spec, absToRel) {
    const base = path.resolve(path.dirname(importerAbsPath), spec);
    for (const suffix of IMPORT_CANDIDATE_SUFFIXES) {
        const candidateAbs = path.normalize(`${base}${suffix}`);
        const relPath = absToRel.get(candidateAbs);
        if (relPath)
            return relPath;
    }
    return null;
}
function normalizedBaseName(relPath) {
    return path.basename(relPath).replace(/\.(test|spec)\.[^.]+$/i, '').replace(/\.[^.]+$/i, '').toLowerCase();
}
function scoreConfigReference(configText, relPath) {
    if (!configText)
        return 0;
    const basename = path.basename(relPath);
    if (configText.includes(relPath))
        return 3;
    if (configText.includes(relPath.replace(/\\/g, '/')))
        return 2;
    if (basename && configText.includes(basename))
        return 1;
    return 0;
}
// ── Semantic similarity ───────────────────────────────────────────────────────
function tokenizeText(text) {
    return text
        .toLowerCase()
        .replace(/[/\\._\-:,;()[\]{}'"`<>@#!?=+*&%$^~]/g, ' ')
        .split(/\s+/)
        .filter(w => w.length > 2 && !GENERIC_TERMS.has(w));
}
function tokenOverlapScore(aTokens, bTokens) {
    if (!aTokens.length || !bTokens.length)
        return 0;
    const bSet = new Set(bTokens);
    let overlap = 0;
    for (const t of aTokens) {
        if (bSet.has(t))
            overlap++;
    }
    const union = aTokens.length + bTokens.length - overlap;
    return union === 0 ? 0 : overlap / union;
}
function extractConceptTerms(tags) {
    return tags
        .map(t => t.replace(/^[#@]/, '').toLowerCase().trim())
        .filter(t => t.length > 3 && !GENERIC_TAGS.has(t) && !GENERIC_TERMS.has(t));
}
// ── Edge helpers ──────────────────────────────────────────────────────────────
function pushEdge(edges, seen, source, target, type, limit, strength) {
    if (!source || !target || source === target || edges.length >= limit)
        return;
    const key = `${source}:${target}:${type}`;
    if (seen.has(key))
        return;
    seen.add(key);
    edges.push({ source, target, type, ...(strength !== undefined ? { strength } : {}) });
}
// ── Route handler ─────────────────────────────────────────────────────────────
export const handleGraphRoutes = async (ctx, req, res, url) => {
    if (req.method === 'POST' && url.pathname === '/api/knowledge-topology/build') {
        const repoIdentity = ctx.getSelectedRepoIdentity(url);
        const repoRoot = typeof repoIdentity?.repoRoot === 'string' && repoIdentity.repoRoot.trim()
            ? repoIdentity.repoRoot.trim()
            : ctx.repoRoot;
        const repoName = typeof repoIdentity?.repoName === 'string' && repoIdentity.repoName.trim()
            ? repoIdentity.repoName.trim()
            : path.basename(repoRoot) || 'workspace';
        void ensureCrGraphBuilt(repoRoot);
        ctx.respondJson(res, { ok: true, status: 'queued', repoRoot, repoName, queuedAt: Date.now() }, 202);
        return true;
    }
    if (req.method === 'GET' && url.pathname === '/api/knowledge-topology') {
        await ctx.respondCachedJson(res, `knowledge-topology:${url.search}`, 60_000, async () => {
            const orchestrator = ctx.getOrchestrator();
            const memory = ctx.getMemory();
            const repoIdentity = ctx.getSelectedRepoIdentity(url);
            const repoRoot = typeof repoIdentity?.repoRoot === 'string' && repoIdentity.repoRoot.trim()
                ? repoIdentity.repoRoot.trim()
                : ctx.repoRoot;
            const workspaceRoot = typeof repoIdentity?.workspaceRoot === 'string' && repoIdentity.workspaceRoot.trim()
                ? repoIdentity.workspaceRoot.trim()
                : repoRoot;
            const repoName = typeof repoIdentity?.repoName === 'string' && repoIdentity.repoName.trim()
                ? repoIdentity.repoName.trim()
                : path.basename(repoRoot) || 'workspace';
            // ── File nodes ─────────────────────────────────────────────────────────
            let filePaths = [];
            try {
                filePaths = await orchestrator?.listTopologyFiles?.(FILE_LIMIT) ?? [];
            }
            catch {
                filePaths = [];
            }
            filePaths = filePaths
                .map((candidate) => path.resolve(String(candidate)))
                .filter((candidate) => candidate === repoRoot || candidate.startsWith(repoRoot + path.sep))
                .slice(0, FILE_LIMIT);
            if (!filePaths.length) {
                filePaths = await scanTopologyFilesFallback(repoRoot, FILE_LIMIT);
            }
            const fileNodes = filePaths.map((absPath) => {
                const relPath = absPath.startsWith(repoRoot + path.sep)
                    ? absPath.slice(repoRoot.length + 1)
                    : path.relative(repoRoot, absPath) || absPath;
                return {
                    id: `file:${relPath}`,
                    type: 'file',
                    kind: classifyFileKind(relPath),
                    label: path.basename(relPath),
                    path: relPath,
                    absPath: path.normalize(absPath),
                    module: extractModulePath(relPath),
                };
            });
            const absToRel = new Map(fileNodes.map((n) => [n.absPath, n.path]));
            const fileRelSet = new Set(fileNodes.map((n) => n.path));
            // Pre-tokenize file paths+labels for similarity scoring
            const fileTokens = new Map();
            for (const node of fileNodes) {
                fileTokens.set(node.id, [...tokenizeText(node.path), ...tokenizeText(node.label)]);
            }
            // ── Module nodes + belongs-to edges ────────────────────────────────────
            const modulePathSet = new Set();
            for (const node of fileNodes) {
                if (node.module)
                    modulePathSet.add(node.module);
            }
            const moduleNodes = [...modulePathSet].sort().map((mod) => ({
                id: `module:${mod}`,
                type: 'module',
                label: path.basename(mod),
                path: mod,
            }));
            const belongsToEdges = [];
            const belongsToSeen = new Set();
            for (const node of fileNodes) {
                if (node.module) {
                    pushEdge(belongsToEdges, belongsToSeen, node.id, `module:${node.module}`, 'belongs-to', MAX_MODULE_EDGES, 1.0);
                }
            }
            // ── Structural edges (imports / tests / configures) ────────────────────
            const structuralEdges = [];
            const structuralSeen = new Set();
            for (const node of fileNodes) {
                if (!['source', 'test', 'config'].includes(node.kind))
                    continue;
                const fileText = await readFileSnippet(node.absPath);
                if (!fileText)
                    continue;
                for (const spec of extractRelativeImports(fileText)) {
                    const targetRel = resolveRelativeTarget(node.absPath, spec, absToRel);
                    if (targetRel) {
                        pushEdge(structuralEdges, structuralSeen, node.id, `file:${targetRel}`, 'imports', MAX_STRUCTURAL_EDGES, 1.0);
                    }
                }
                if (node.kind === 'test') {
                    const affinityMatches = fileNodes
                        .filter((c) => c.kind === 'source' && normalizedBaseName(c.path) === normalizedBaseName(node.path))
                        .sort((a, b) => a.path.length - b.path.length)
                        .slice(0, 2);
                    for (const m of affinityMatches) {
                        pushEdge(structuralEdges, structuralSeen, node.id, m.id, 'tests', MAX_STRUCTURAL_EDGES, 0.9);
                    }
                }
                if (node.kind === 'config') {
                    const configMatches = fileNodes
                        .filter((c) => c.kind === 'source')
                        .map((c) => ({ c, score: scoreConfigReference(fileText, c.path) }))
                        .filter((e) => e.score > 0)
                        .sort((a, b) => b.score - a.score || a.c.path.length - b.c.path.length)
                        .slice(0, 4);
                    for (const { c, score } of configMatches) {
                        pushEdge(structuralEdges, structuralSeen, node.id, c.id, 'configures', MAX_STRUCTURAL_EDGES, score / 3);
                    }
                }
            }
            // ── Memory nodes ───────────────────────────────────────────────────────
            const snapshots = memory?.listSnapshots?.(MEMORY_LIMIT, { state: 'active' }) ?? [];
            const memoryNodes = snapshots.map((s) => ({
                id: `memory:${s.id}`,
                type: 'memory',
                label: (s.excerpt ?? '').slice(0, 60) || s.id.slice(0, 16),
                tier: s.tier ?? 'cortex',
                priority: s.priority ?? 0.3,
                tags: Array.isArray(s.tags) ? s.tags : [],
                promoted: s.tier === 'hippocampus' || s.tier === 'cortex',
            }));
            // ── Memory → file overlay (token similarity, not fragile string match) ─
            const memoryOverlayEdges = [];
            const memoryOverlaySeen = new Set();
            for (const snapshot of snapshots) {
                const excerpt = String(snapshot.excerpt ?? '');
                const excerptTokens = tokenizeText(excerpt);
                const scored = [];
                for (const node of fileNodes) {
                    // Exact path match first (high confidence)
                    if (excerpt.includes(node.path) || excerpt.includes(path.basename(node.path))) {
                        scored.push({ nodeId: node.id, score: 0.95 });
                        continue;
                    }
                    // Token overlap (semantic match)
                    const fTokens = fileTokens.get(node.id) ?? [];
                    const sim = tokenOverlapScore(excerptTokens, fTokens);
                    if (sim >= MEMORY_FILE_SIM_THRESHOLD) {
                        scored.push({ nodeId: node.id, score: sim });
                    }
                }
                // Cap at top 3 links per memory — keeps graph readable
                scored.sort((a, b) => b.score - a.score);
                for (const { nodeId, score } of scored.slice(0, 3)) {
                    pushEdge(memoryOverlayEdges, memoryOverlaySeen, `memory:${snapshot.id}`, nodeId, 'memory-overlay', MAX_MEMORY_OVERLAY_EDGES, score);
                }
            }
            // ── Memory ↔ memory association (specific tags + excerpt similarity) ───
            const memoryAssocEdges = [];
            const memoryAssocSeen = new Set();
            const memCache = snapshots.map((s, i) => ({
                id: memoryNodes[i]?.id ?? `memory:${s.id}`,
                tokens: tokenizeText(String(s.excerpt ?? '')),
                specificTags: (Array.isArray(s.tags) ? s.tags : [])
                    .map((t) => t.replace(/^[#@]/, '').toLowerCase())
                    .filter((t) => t.length > 3 && !GENERIC_TAGS.has(t)),
            }));
            for (let i = 0; i < memCache.length && memoryAssocEdges.length < MAX_MEMORY_ASSOC_EDGES; i++) {
                for (let j = i + 1; j < memCache.length && memoryAssocEdges.length < MAX_MEMORY_ASSOC_EDGES; j++) {
                    const a = memCache[i];
                    const b = memCache[j];
                    const sharedTags = a.specificTags.filter((t) => b.specificTags.includes(t)).length;
                    const textSim = tokenOverlapScore(a.tokens, b.tokens);
                    if (sharedTags >= 1 || textSim >= MEMORY_ASSOC_SIM_THRESHOLD) {
                        const strength = Math.min(1, sharedTags * 0.35 + textSim);
                        pushEdge(memoryAssocEdges, memoryAssocSeen, a.id, b.id, 'memory-association', MAX_MEMORY_ASSOC_EDGES, strength);
                    }
                }
            }
            // ── Concept nodes (prominent memory tags, ≥2 occurrences) ──────────────
            const conceptCounts = new Map();
            for (const snapshot of snapshots) {
                const tags = Array.isArray(snapshot.tags) ? snapshot.tags : [];
                for (const term of extractConceptTerms(tags)) {
                    conceptCounts.set(term, (conceptCounts.get(term) ?? 0) + 1);
                }
            }
            const conceptTerms = [...conceptCounts.entries()]
                .filter(([, count]) => count >= CONCEPT_MIN_OCCURRENCES)
                .sort(([, a], [, b]) => b - a)
                .slice(0, CONCEPT_MAX)
                .map(([term]) => term);
            const conceptNodes = conceptTerms.map((term) => ({
                id: `concept:${term}`,
                type: 'concept',
                label: term,
                terms: [term],
            }));
            // concept-of edges: memory → concept (via tags)
            const conceptEdges = [];
            const conceptEdgesSeen = new Set();
            for (const snapshot of snapshots) {
                const tags = Array.isArray(snapshot.tags) ? snapshot.tags : [];
                for (const term of extractConceptTerms(tags)) {
                    if ((conceptCounts.get(term) ?? 0) >= CONCEPT_MIN_OCCURRENCES) {
                        pushEdge(conceptEdges, conceptEdgesSeen, `memory:${snapshot.id}`, `concept:${term}`, 'concept-of', MAX_CONCEPT_EDGES, 0.8);
                    }
                }
            }
            // file → concept (via file name token match)
            for (const node of fileNodes) {
                const fTokens = fileTokens.get(node.id) ?? [];
                for (const term of conceptTerms) {
                    if (fTokens.includes(term)) {
                        pushEdge(conceptEdges, conceptEdgesSeen, node.id, `concept:${term}`, 'concept-of', MAX_CONCEPT_EDGES, 0.7);
                    }
                }
            }
            // ── Assemble ───────────────────────────────────────────────────────────
            const edges = [...belongsToEdges, ...structuralEdges, ...memoryOverlayEdges, ...memoryAssocEdges, ...conceptEdges];
            const graphSource = structuralEdges.length > 0
                ? 'heuristic-local-relationships'
                : fileNodes.length > 0
                    ? 'runtime-topology-shell'
                    : memoryOverlayEdges.length > 0 || memoryNodes.length > 0
                        ? 'memory-overlay-fallback'
                        : 'empty';
            const fallbackMode = graphSource === 'heuristic-local-relationships' ? 'none'
                : graphSource === 'runtime-topology-shell' ? 'synthetic'
                    : graphSource === 'memory-overlay-fallback' ? 'topology-missing'
                        : 'empty';
            const sources = [
                fileNodes.length > 0 ? 'runtime-topology-files' : null,
                moduleNodes.length > 0 ? 'module-grouping' : null,
                structuralEdges.some((e) => e.type === 'imports') ? 'heuristic-imports' : null,
                structuralEdges.some((e) => e.type === 'tests') ? 'heuristic-test-affinity' : null,
                structuralEdges.some((e) => e.type === 'configures') ? 'heuristic-config-affinity' : null,
                memoryOverlayEdges.length > 0 ? 'memory-overlay-similarity' : null,
                memoryAssocEdges.length > 0 ? 'memory-association' : null,
                conceptNodes.length > 0 ? 'concept-extraction' : null,
            ].filter((v) => Boolean(v));
            // Deterministic ordering — stable across refreshes
            const sortedFileNodes = [...fileNodes].sort((a, b) => a.id.localeCompare(b.id));
            const sortedModuleNodes = [...moduleNodes].sort((a, b) => a.id.localeCompare(b.id));
            const sortedMemoryNodes = [...memoryNodes].sort((a, b) => String(a.id).localeCompare(String(b.id)));
            const sortedConceptNodes = [...conceptNodes].sort((a, b) => a.id.localeCompare(b.id));
            const sortedEdges = [...edges].sort((a, b) => `${a.source}|${a.target}|${a.type}`.localeCompare(`${b.source}|${b.target}|${b.type}`));
            return {
                generatedAt: Date.now(),
                repoName,
                repoRoot,
                nodes: [...sortedFileNodes, ...sortedModuleNodes, ...sortedMemoryNodes, ...sortedConceptNodes],
                edges: sortedEdges,
                fallbackMode,
                provenance: {
                    graphSource,
                    fallbackMode,
                    freshness: 'live',
                    sources,
                    repoIdentity: {
                        repoName,
                        repoRoot,
                        workspaceRoot,
                        workspaceSource: typeof repoIdentity?.workspaceSource === 'string' ? repoIdentity.workspaceSource : null,
                        workspaceStateKey: typeof repoIdentity?.workspaceStateKey === 'string' ? repoIdentity.workspaceStateKey : null,
                        remoteUrl: repoIdentity?.remoteUrl ?? null,
                    },
                },
                meta: {
                    fileCount: fileNodes.length,
                    moduleCount: moduleNodes.length,
                    memoryCount: memoryNodes.length,
                    conceptCount: conceptNodes.length,
                    structuralEdgeCount: structuralEdges.length,
                    belongsToEdgeCount: belongsToEdges.length,
                    memoryOverlayEdgeCount: memoryOverlayEdges.length,
                    memoryAssociationEdgeCount: memoryAssocEdges.length,
                    conceptEdgeCount: conceptEdges.length,
                    edgeCount: edges.length,
                    graphSource,
                    fallbackMode,
                    freshness: 'live',
                },
            };
        });
        return true;
    }
    return false;
};
