// engine/dependency-health.ts — Dependency health analyzer (SPEC-967)
// Detects unused, missing, outdated, duplicated, and conflicting dependencies.
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { glob } from 'glob';
/**
 * Analyze dependency health for a project.
 */
export async function analyzeDependencyHealth(opts) {
    const { projectPath, ecosystem = 'npm' } = opts;
    if (ecosystem !== 'npm') {
        return emptyResult('Only npm ecosystem is currently supported.');
    }
    const pkgJson = await readPackageJson(projectPath);
    const lockfile = await readPackageLock(projectPath);
    const imports = await scanImports(projectPath);
    const unused = findUnused(pkgJson, imports);
    const missing = findMissing(pkgJson, imports);
    const peerConflicts = findPeerConflicts(pkgJson, lockfile);
    const duplicated = findDuplicated(lockfile);
    const outdated = await findOutdated(pkgJson, opts.fetchLatestVersion ?? fetchNpmLatest);
    const bloatScore = computeBloatScore(pkgJson);
    const recommendations = buildRecommendations(unused, missing, peerConflicts, outdated, duplicated);
    const score = computeScore(unused, missing, peerConflicts, outdated, duplicated, bloatScore);
    return {
        score,
        unused,
        missing,
        peerConflicts,
        outdated,
        duplicated,
        bloatScore,
        recommendations,
    };
}
async function readPackageJson(projectPath) {
    try {
        const raw = await readFile(join(projectPath, 'package.json'), 'utf-8');
        return JSON.parse(raw);
    }
    catch {
        return {};
    }
}
async function readPackageLock(projectPath) {
    try {
        const raw = await readFile(join(projectPath, 'package-lock.json'), 'utf-8');
        return JSON.parse(raw);
    }
    catch {
        return {};
    }
}
function allDeps(pkg) {
    return {
        ...(pkg.dependencies ?? {}),
        ...(pkg.devDependencies ?? {}),
    };
}
// ---------------------------------------------------------------------------
// Import scanning
// ---------------------------------------------------------------------------
const IMPORT_RE = /(?:^|[;\s])import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+)?['"]([^'"]+)['"];?/gm;
const REQUIRE_RE = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/gm;
async function scanImports(projectPath) {
    const result = new Map();
    const files = await glob('src/**/*.{ts,tsx,js,jsx}', {
        cwd: projectPath,
        absolute: true,
    });
    for (const file of files) {
        let raw;
        try {
            raw = await readFile(file, 'utf-8');
        }
        catch {
            continue;
        }
        for (const re of [IMPORT_RE, REQUIRE_RE]) {
            let match;
            while ((match = re.exec(raw)) !== null) {
                const source = match[1] ?? '';
                const pkgName = extractPackageName(source);
                if (!pkgName) {
                    continue;
                }
                const existing = result.get(pkgName);
                if (existing) {
                    existing.add(file);
                }
                else {
                    result.set(pkgName, new Set([file]));
                }
            }
        }
    }
    return result;
}
/** Extract npm package name from an import source. */
function extractPackageName(source) {
    if (source.startsWith('.') || source.startsWith('/')) {
        return null;
    }
    if (source.startsWith('node:')) {
        return null;
    }
    // Scoped: @scope/name or @scope/name/subpath
    if (source.startsWith('@')) {
        const parts = source.split('/');
        if (parts.length >= 2) {
            return `${parts[0]}/${parts[1]}`;
        }
        return null;
    }
    // Unscoped: name or name/subpath
    const parts = source.split('/');
    return parts[0] ?? null;
}
// ---------------------------------------------------------------------------
// Unused deps
// ---------------------------------------------------------------------------
function findUnused(pkg, imports) {
    const deps = allDeps(pkg);
    const unused = [];
    for (const [name, version] of Object.entries(deps)) {
        if (!imports.has(name)) {
            unused.push({
                name,
                version,
                suggestion: `Remove from package.json — no imports found in src/`,
            });
        }
    }
    return unused;
}
// ---------------------------------------------------------------------------
// Missing deps
// ---------------------------------------------------------------------------
function findMissing(pkg, imports) {
    const deps = allDeps(pkg);
    const missing = [];
    for (const [name, files] of imports.entries()) {
        if (!(name in deps)) {
            missing.push({
                name,
                importers: Array.from(files).map((f) => f.replace(/\\/g, '/')),
            });
        }
    }
    return missing;
}
// ---------------------------------------------------------------------------
// Peer conflicts
// ---------------------------------------------------------------------------
function findPeerConflicts(pkg, lock) {
    const peers = pkg.peerDependencies ?? {};
    const conflicts = [];
    if (!lock.packages && !lock.dependencies) {
        return conflicts;
    }
    const installed = new Map();
    // v2/v3 lockfile
    if (lock.packages) {
        for (const [key, entry] of Object.entries(lock.packages)) {
            if (!key || key === '') {
                continue;
            }
            const name = extractNameFromPackagesKey(key);
            if (name && entry.version) {
                installed.set(name, entry.version);
            }
        }
    }
    // v1 lockfile
    if (lock.dependencies) {
        flattenLockDeps(lock.dependencies, installed);
    }
    for (const [peerName, expectedRange] of Object.entries(peers)) {
        const foundVersion = installed.get(peerName);
        if (!foundVersion) {
            continue;
        }
        if (!satisfiesRange(foundVersion, expectedRange)) {
            conflicts.push({
                package: peerName,
                expected: expectedRange,
                found: foundVersion,
            });
        }
    }
    return conflicts;
}
function extractNameFromPackagesKey(key) {
    const parts = key.split('node_modules/');
    const lastPart = parts[parts.length - 1];
    if (!lastPart) {
        return null;
    }
    return lastPart;
}
function flattenLockDeps(deps, result) {
    for (const [name, entry] of Object.entries(deps)) {
        if (entry.version && !result.has(name)) {
            result.set(name, entry.version);
        }
        if (entry.dependencies && typeof entry.dependencies === 'object') {
            flattenLockDeps(entry.dependencies, result);
        }
    }
}
/** Very loose semver range check for peer conflicts. */
function satisfiesRange(version, range) {
    const v = parseSemver(version);
    if (!v) {
        return true; // can't parse = assume ok
    }
    // Simple handling for ^, ~, >=, >, <, <=, and exact
    const cleanRange = range.trim();
    if (cleanRange.startsWith('^')) {
        const target = parseSemver(cleanRange.slice(1));
        if (!target) {
            return true;
        }
        return v.major === target.major && gte(v, target);
    }
    if (cleanRange.startsWith('~')) {
        const target = parseSemver(cleanRange.slice(1));
        if (!target) {
            return true;
        }
        return v.major === target.major && v.minor === target.minor && gte(v, target);
    }
    if (cleanRange.startsWith('>=')) {
        const target = parseSemver(cleanRange.slice(2));
        if (!target) {
            return true;
        }
        return gte(v, target);
    }
    if (cleanRange.startsWith('>')) {
        const target = parseSemver(cleanRange.slice(1));
        if (!target) {
            return true;
        }
        return gt(v, target);
    }
    if (cleanRange.startsWith('<=')) {
        const target = parseSemver(cleanRange.slice(2));
        if (!target) {
            return true;
        }
        return lte(v, target);
    }
    if (cleanRange.startsWith('<')) {
        const target = parseSemver(cleanRange.slice(1));
        if (!target) {
            return true;
        }
        return lt(v, target);
    }
    // Exact or other
    const target = parseSemver(cleanRange);
    if (!target) {
        return true;
    }
    return v.major === target.major && v.minor === target.minor && v.patch === target.patch;
}
function parseSemver(version) {
    const clean = version.replace(/^[\^~>=<]+/, '').replace(/^v/, '');
    const SEMVER_RE = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?/;
    const m = SEMVER_RE.exec(clean);
    if (!m) {
        return null;
    }
    return {
        major: Number(m[1]),
        minor: m[2] !== undefined ? Number(m[2]) : 0,
        patch: m[3] !== undefined ? Number(m[3]) : 0,
    };
}
function gte(a, b) {
    if (a.major !== b.major) {
        return a.major > b.major;
    }
    if (a.minor !== b.minor) {
        return a.minor > b.minor;
    }
    return a.patch >= b.patch;
}
function gt(a, b) {
    if (a.major !== b.major) {
        return a.major > b.major;
    }
    if (a.minor !== b.minor) {
        return a.minor > b.minor;
    }
    return a.patch > b.patch;
}
function lte(a, b) {
    return !gt(a, b);
}
function lt(a, b) {
    return !gte(a, b);
}
// ---------------------------------------------------------------------------
// Duplicated deps in lockfile
// ---------------------------------------------------------------------------
function findDuplicated(lock) {
    const versionsByName = new Map();
    if (lock.packages) {
        for (const [key, entry] of Object.entries(lock.packages)) {
            if (!key || key === '') {
                continue;
            }
            const name = extractNameFromPackagesKey(key);
            if (!name || !entry.version) {
                continue;
            }
            const existing = versionsByName.get(name);
            if (existing) {
                existing.add(entry.version);
            }
            else {
                versionsByName.set(name, new Set([entry.version]));
            }
        }
    }
    if (lock.dependencies) {
        collectVersions(lock.dependencies, versionsByName);
    }
    const duplicated = [];
    for (const [name, versions] of versionsByName.entries()) {
        if (versions.size > 1) {
            duplicated.push({ name, versions: Array.from(versions) });
        }
    }
    return duplicated;
}
function collectVersions(deps, result) {
    for (const [name, entry] of Object.entries(deps)) {
        if (entry.version) {
            const existing = result.get(name);
            if (existing) {
                existing.add(entry.version);
            }
            else {
                result.set(name, new Set([entry.version]));
            }
        }
        if (entry.dependencies && typeof entry.dependencies === 'object') {
            collectVersions(entry.dependencies, result);
        }
    }
}
// ---------------------------------------------------------------------------
// Outdated deps (registry check)
// ---------------------------------------------------------------------------
async function findOutdated(pkg, fetchLatest) {
    const deps = allDeps(pkg);
    const outdated = [];
    for (const [name, currentRange] of Object.entries(deps)) {
        const latest = await fetchLatest(name);
        if (!latest) {
            continue;
        }
        const current = parseSemver(currentRange);
        const latestV = parseSemver(latest);
        if (!current || !latestV) {
            continue;
        }
        if (gt(latestV, current)) {
            let severity;
            if (latestV.major > current.major) {
                severity = 'major';
            }
            else if (latestV.minor > current.minor) {
                severity = 'minor';
            }
            else {
                severity = 'patch';
            }
            outdated.push({
                name,
                current: currentRange,
                latest,
                severity,
            });
        }
    }
    return outdated;
}
/** Fetch latest version from npm registry. */
async function fetchNpmLatest(name) {
    try {
        const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}/latest`, {
            signal: AbortSignal.timeout(5000),
        });
        if (!res.ok) {
            return null;
        }
        const data = (await res.json());
        return typeof data.version === 'string' ? data.version : null;
    }
    catch {
        return null;
    }
}
// ---------------------------------------------------------------------------
// Bloat score
// ---------------------------------------------------------------------------
function computeBloatScore(pkg) {
    const depCount = Object.keys(pkg.dependencies ?? {}).length;
    const devCount = Object.keys(pkg.devDependencies ?? {}).length;
    // Heuristic: more than 50 prod deps = high bloat
    const total = depCount + devCount;
    if (total <= 10) {
        return 0;
    }
    if (total <= 25) {
        return 20;
    }
    if (total <= 50) {
        return 40;
    }
    if (total <= 75) {
        return 60;
    }
    return 80;
}
// ---------------------------------------------------------------------------
// Score & recommendations
// ---------------------------------------------------------------------------
function computeScore(unused, missing, peerConflicts, outdated, duplicated, bloatScore) {
    let score = 100;
    score -= unused.length * 3;
    score -= missing.length * 5;
    score -= peerConflicts.length * 5;
    score -= outdated.filter((o) => o.severity === 'major').length * 4;
    score -= outdated.filter((o) => o.severity === 'minor').length * 2;
    score -= outdated.filter((o) => o.severity === 'patch').length * 1;
    score -= duplicated.length * 4;
    score -= Math.floor(bloatScore / 10);
    return Math.max(0, Math.min(100, score));
}
function buildRecommendations(unused, missing, peerConflicts, outdated, duplicated) {
    const recs = [];
    if (unused.length > 0) {
        recs.push(`Remove ${String(unused.length)} unused package(s) to reduce install size.`);
    }
    if (missing.length > 0) {
        recs.push(`Add ${String(missing.length)} missing package(s) to dependencies.`);
    }
    if (peerConflicts.length > 0) {
        recs.push(`Resolve ${String(peerConflicts.length)} peer dependency conflict(s).`);
    }
    if (outdated.length > 0) {
        const majors = outdated.filter((o) => o.severity === 'major').length;
        if (majors > 0) {
            recs.push(`Update ${String(majors)} package(s) with major version bumps — review breaking changes.`);
        }
        const minors = outdated.filter((o) => o.severity === 'minor').length;
        if (minors > 0) {
            recs.push(`Update ${String(minors)} package(s) with minor version bumps for new features.`);
        }
    }
    if (duplicated.length > 0) {
        recs.push(`Deduplicate ${String(duplicated.length)} package(s) by regenerating the lockfile (npm dedupe or rm -rf node_modules && npm install).`);
    }
    if (recs.length === 0) {
        recs.push('All dependency health checks passed. Great job!');
    }
    return recs;
}
function emptyResult(message) {
    return {
        score: 100,
        unused: [],
        missing: [],
        peerConflicts: [],
        outdated: [],
        duplicated: [],
        bloatScore: 0,
        recommendations: [message],
    };
}
//# sourceMappingURL=dependency-health.js.map