/**
 * Wiki v2 module builder — assembles ProjectOverview, ModuleMetadata[], and
 * the v2 WikiManifest writer. Framework-aware description cascade lives in
 * wiki-cascade.ts; raw parsers and key-dep selector in wiki-overview-sources.ts.
 */
import { readFileSync } from "node:fs";
import { basename, join } from "node:path";
import { isShallowClone, parseGoMod, parsePyprojectToml, parseCargoToml, selectKeyDependencies, } from "./wiki-overview-sources.js";
import { buildUniqueSlugs, buildFileToCommunityMap, } from "./wiki-manifest.js";
/**
 * Build a ProjectOverview from the analyzeProject result and the code index.
 * Emits `_degraded` as a side-channel when git history is shallow/missing; the
 * orchestrator strips it and pushes to `degraded_reasons`.
 */
export function buildProjectOverview(projectResult, codeIndex) {
    const ident = projectResult.identity;
    const stack = projectResult.stack;
    const name = ident?.project_name ?? basename(codeIndex.root);
    const project_type = ident?.project_type ?? "single";
    const workspaces = stack?.monorepo?.workspaces ?? [];
    const scripts = readScripts(codeIndex.root);
    const fallbackName = nonJsFallbackName(codeIndex.root);
    const dependencies = selectKeyDependencies(projectResult);
    mergeNonJsDeps(dependencies, codeIndex.root);
    const gh = projectResult.git_health ?? null;
    const shallow = isShallowClone(codeIndex.root);
    const totalCommits = shallow || gh === null ? null : (gh?.total_commits ?? null);
    const contributors = shallow || gh === null ? null : (gh?.contributors ?? null);
    const overview = {
        name: name ?? fallbackName ?? "unknown",
        git_remote: ident?.git_remote ?? null,
        project_type,
        stack: {
            language: stack?.language ?? inferLanguage(codeIndex.root) ?? "unknown",
            language_version: stack?.language_version ?? null,
            framework: stack?.framework ?? null,
            framework_version: stack?.framework_version ?? null,
            test_runner: stack?.test_runner ?? null,
            package_manager: stack?.package_manager ?? null,
            build_tool: stack?.build_tool ?? null,
        },
        scripts,
        entry_points: projectResult.dependency_graph?.entry_points ?? [],
        workspaces,
        dependencies,
        known_gotchas: (projectResult.known_gotchas?.auto_detected ?? []).map((g) => ({
            gotcha: g.gotcha,
            severity: g.severity,
        })),
        stats: {
            total_files: codeIndex.file_count,
            total_commits: totalCommits,
            contributors,
        },
    };
    if (shallow || gh === null) {
        overview._degraded = "shallow_clone_or_insufficient_history";
    }
    return overview;
}
function readScripts(root) {
    try {
        const raw = readFileSync(join(root, "package.json"), "utf-8");
        const pkg = JSON.parse(raw);
        return pkg.scripts ?? {};
    }
    catch {
        return {};
    }
}
function nonJsFallbackName(root) {
    try {
        const goMod = readFileSync(join(root, "go.mod"), "utf-8");
        const p = parseGoMod(goMod);
        if (p.name)
            return basename(p.name);
    }
    catch { /* no go.mod */ }
    try {
        const py = readFileSync(join(root, "pyproject.toml"), "utf-8");
        const p = parsePyprojectToml(py);
        if (p.name)
            return p.name;
    }
    catch { /* no pyproject */ }
    try {
        const cargo = readFileSync(join(root, "Cargo.toml"), "utf-8");
        const p = parseCargoToml(cargo);
        if (p.name)
            return p.name;
    }
    catch { /* no cargo */ }
    return null;
}
function mergeNonJsDeps(summary, root) {
    if (summary.key.length > 0)
        return;
    try {
        const py = readFileSync(join(root, "pyproject.toml"), "utf-8");
        const p = parsePyprojectToml(py);
        summary.prod_total = Math.max(summary.prod_total, p.deps.length);
        for (const name of p.deps.slice(0, 15)) {
            summary.key.push({ name, version: "", kind: "prod" });
        }
        return;
    }
    catch { /* skip */ }
    try {
        const goMod = readFileSync(join(root, "go.mod"), "utf-8");
        const p = parseGoMod(goMod);
        summary.prod_total = Math.max(summary.prod_total, p.deps.length);
        for (const name of p.deps.slice(0, 15)) {
            summary.key.push({ name, version: "", kind: "prod" });
        }
    }
    catch { /* skip */ }
}
function inferLanguage(root) {
    try {
        readFileSync(join(root, "go.mod"), "utf-8");
        return "Go";
    }
    catch { }
    try {
        readFileSync(join(root, "pyproject.toml"), "utf-8");
        return "Python";
    }
    catch { }
    try {
        readFileSync(join(root, "Cargo.toml"), "utf-8");
        return "Rust";
    }
    catch { }
    return null;
}
// ---------------------------------------------------------------------------
// ModuleMetadata assembly (cascade + per-community data)
// ---------------------------------------------------------------------------
const TEST_FILE_RE = /\.test\.(ts|js|py|go)$|\.spec\.(ts|js)$|__tests__\//;
export function buildModuleMetadata(communities, projectResult, codeIndex, importEdges, fileHotspots, rankedHubs) {
    void rankedHubs;
    const slugs = buildUniqueSlugs(communities);
    const fileToCommunity = buildFileToCommunityMap(communities, slugs);
    const hotspotFiles = new Set(fileHotspots.slice(0, 20).map((h) => h.file));
    const symbolsByFile = indexSymbolsByFile(codeIndex.symbols);
    const out = [];
    for (const c of communities) {
        const slug = slugs.get(c.name);
        const isTestCommunity = c.files.length > 0 && c.files.every((f) => TEST_FILE_RE.test(f));
        const microModule = c.files.length < 4;
        const description = describeCommunity(c, projectResult, isTestCommunity, importEdges, fileToCommunity);
        const role = isTestCommunity ? "tests"
            : microModule ? "micro-module"
                : inferRole(c, projectResult);
        const { key_exports, approximate } = collectKeyExports(c, symbolsByFile, importEdges);
        const has_hotspot = c.files.some((f) => hotspotFiles.has(f));
        const { depends_on, depended_by } = computeCommunityEdges(c, importEdges, fileToCommunity, slug);
        const module = {
            slug,
            name: c.name,
            description,
            role,
            files: c.files.length,
            cohesion: c.cohesion ?? 0,
            key_exports,
            depends_on,
            depended_by,
            has_hotspot,
        };
        if (approximate)
            module.key_exports_approximate = true;
        out.push(module);
    }
    return out;
}
function indexSymbolsByFile(symbols) {
    const map = new Map();
    for (const s of symbols) {
        const arr = map.get(s.file);
        if (arr)
            arr.push(s);
        else
            map.set(s.file, [s]);
    }
    return map;
}
function collectKeyExports(c, symbolsByFile, importEdges) {
    const candidates = [];
    for (const f of c.files) {
        for (const s of symbolsByFile.get(f) ?? []) {
            if (s.is_exported === true)
                candidates.push(s);
        }
    }
    if (candidates.length > 0) {
        const byFanIn = rankByExternalImport(candidates, importEdges, c.files);
        return {
            key_exports: byFanIn.slice(0, 5).map(toKeyExport),
            approximate: false,
        };
    }
    // Fallback: community files imported from outside → surface first symbol per file
    const externallyImported = new Set();
    const fileSet = new Set(c.files);
    for (const e of importEdges) {
        if (fileSet.has(e.to) && !fileSet.has(e.from))
            externallyImported.add(e.to);
    }
    const approx = [];
    for (const f of externallyImported) {
        const syms = symbolsByFile.get(f) ?? [];
        const first = syms.find((s) => EXPORT_KINDS.has(s.kind));
        if (first)
            approx.push(first);
    }
    return {
        key_exports: approx.slice(0, 5).map(toKeyExport),
        approximate: approx.length > 0,
    };
}
const EXPORT_KINDS = new Set(["function", "class", "interface", "type", "component", "hook"]);
function toKeyExport(s) {
    const kind = EXPORT_KINDS.has(s.kind) ? s.kind : "function";
    const out = { name: s.name, kind, file: s.file };
    if (s.signature)
        out.signature = s.signature;
    return out;
}
function rankByExternalImport(syms, edges, communityFiles) {
    const fileSet = new Set(communityFiles);
    const externalImporters = new Map();
    for (const e of edges) {
        if (fileSet.has(e.to) && !fileSet.has(e.from)) {
            externalImporters.set(e.to, (externalImporters.get(e.to) ?? 0) + 1);
        }
    }
    return [...syms].sort((a, b) => (externalImporters.get(b.file) ?? 0) - (externalImporters.get(a.file) ?? 0));
}
function computeCommunityEdges(c, edges, fileToCommunity, ownSlug) {
    const fileSet = new Set(c.files);
    const dependsOn = new Set();
    const dependedBy = new Set();
    for (const e of edges) {
        const fromSlug = fileToCommunity[e.from];
        const toSlug = fileToCommunity[e.to];
        if (fromSlug === ownSlug && toSlug && toSlug !== ownSlug)
            dependsOn.add(toSlug);
        if (toSlug === ownSlug && fromSlug && fromSlug !== ownSlug)
            dependedBy.add(fromSlug);
        // fileSet referenced to preserve intent
        void fileSet;
    }
    return {
        depends_on: [...dependsOn].sort(),
        depended_by: [...dependedBy].sort(),
    };
}
function inferRole(c, projectResult) {
    const dir = commonDirPrefix(c.files);
    if (/routes|controller|handlers?/.test(dir))
        return "framework-routes";
    if (/components?|ui|views/.test(dir))
        return "framework-components";
    if (/utils?|helpers?|lib/.test(dir))
        return "utilities";
    if (/parser|extract/.test(dir))
        return "parsers";
    if (/storage|db|repository/.test(dir))
        return "storage";
    if (/search|query|index/.test(dir))
        return "search";
    if (/cli|bin/.test(dir))
        return "cli";
    if (projectResult.stack?.framework)
        return "framework-tools";
    return "core-library";
}
function commonDirPrefix(files) {
    if (files.length === 0)
        return "";
    const parts = files[0].split("/");
    for (const f of files.slice(1)) {
        const fp = f.split("/");
        while (parts.length > 0 && parts[0] !== fp[0])
            parts.shift();
        if (parts.length === 0)
            break;
    }
    return parts.join("/");
}
// Description cascade: framework → dep-lookup → file-pattern/keyword
function describeCommunity(c, projectResult, isTestCommunity, importEdges, fileToCommunity) {
    if (isTestCommunity)
        return describeTestCommunity(c, importEdges, fileToCommunity);
    try {
        const lvl1 = describeViaFramework(c, projectResult);
        if (lvl1)
            return lvl1;
    }
    catch { /* advance */ }
    try {
        const lvl2 = describeViaDependencyLookup(c, projectResult);
        if (lvl2)
            return lvl2;
    }
    catch { /* advance */ }
    return describeViaFilePatterns(c);
}
function describeTestCommunity(c, importEdges, fileToCommunity) {
    const fileSet = new Set(c.files);
    const outgoing = new Map();
    for (const e of importEdges) {
        if (!fileSet.has(e.from))
            continue;
        const target = fileToCommunity[e.to];
        if (target)
            outgoing.set(target, (outgoing.get(target) ?? 0) + 1);
    }
    const top = [...outgoing.entries()].sort((a, b) => b[1] - a[1])[0];
    const target = top?.[0] ?? "this project";
    return `Test suite for ${target}`;
}
function describeViaFramework(c, p) {
    const dir = commonDirPrefix(c.files);
    const fw = p.stack?.framework;
    if (!fw)
        return null;
    if (/controller/.test(dir) && fw.toLowerCase().includes("nest")) {
        return `NestJS controllers — ${c.files.length} files handling HTTP routes.`;
    }
    if (/pages|app/.test(dir) && fw.toLowerCase().includes("next")) {
        return `Next.js routes and layouts — ${c.files.length} files.`;
    }
    if (/routes|handlers?/.test(dir) && fw.toLowerCase().includes("hono")) {
        return `Hono route handlers — ${c.files.length} files.`;
    }
    if (/components?/.test(dir) && fw.toLowerCase().includes("react")) {
        return `React components — ${c.files.length} files.`;
    }
    return null;
}
const DEP_ROLES = {
    prisma: "Prisma data access", "@prisma/client": "Prisma client integration",
    pg: "Postgres data access", mongoose: "MongoDB data access",
    "drizzle-orm": "Drizzle ORM data access",
    redis: "Redis integration", ioredis: "Redis integration",
    zod: "Zod validation schemas",
    "@tanstack/react-query": "Data fetching via TanStack Query",
    "graphql": "GraphQL integration",
};
function describeViaDependencyLookup(c, p) {
    const kv = p.dependency_health?.key_versions ?? {};
    for (const pkg of Object.keys(kv)) {
        if (pkg in DEP_ROLES && pkg_keywordsInFiles(pkg, c.files)) {
            return `${DEP_ROLES[pkg]} — ${c.files.length} files.`;
        }
    }
    return null;
}
function pkg_keywordsInFiles(pkg, files) {
    const base = pkg.replace(/^@/, "").split("/")[0].toLowerCase();
    return files.some((f) => f.toLowerCase().includes(base));
}
function describeViaFilePatterns(c) {
    const dir = commonDirPrefix(c.files);
    if (/utils?|helpers?/.test(dir))
        return `Utilities — ${c.files.length} helper files under ${dir || "this module"}.`;
    if (/parser|extractor/.test(dir))
        return `Parsers and extractors — ${c.files.length} files under ${dir}.`;
    if (/storage/.test(dir))
        return `Storage layer — ${c.files.length} files.`;
    if (/tools?/.test(dir))
        return `Tooling — ${c.files.length} files under ${dir}.`;
    return `Module of ${c.files.length} files${dir ? ` under ${dir}` : ""}.`;
}
export function buildWikiManifestV2(options) {
    const { index_hash, git_commit, pages, communities, project, modules, oldManifest, degradedReasons, modules_truncated, truncation_reason, } = options;
    const file_to_community = buildFileToCommunityMap(communities);
    const token_estimates = {};
    const CHARS_PER_TOKEN = 4;
    for (const p of pages)
        token_estimates[p.slug] = Math.ceil(p.content.length / CHARS_PER_TOKEN);
    const slug_redirects = {
        ...(oldManifest?.slug_redirects ?? {}),
    };
    const linkRe = /\[\[([^\]]+)\]\]/g;
    const builtPages = pages.map((p) => {
        const outbound_links = [];
        linkRe.lastIndex = 0;
        let m;
        while ((m = linkRe.exec(p.content)) !== null) {
            if (m[1] !== undefined)
                outbound_links.push(m[1]);
        }
        return { slug: p.slug, title: p.title, type: p.type, file: p.file, outbound_links };
    });
    const degraded = (degradedReasons?.length ?? 0) > 0;
    return {
        schema_version: 2,
        manifest_schema_version: "2.1.0",
        generated_at: new Date().toISOString(),
        index_hash,
        git_commit,
        project,
        modules,
        pages: builtPages,
        slug_redirects,
        token_estimates,
        file_to_community,
        degraded,
        ...(degraded ? { degraded_reasons: degradedReasons } : {}),
        ...(modules_truncated ? { modules_truncated: true } : {}),
        ...(truncation_reason ? { truncation_reason } : {}),
    };
}
export function buildWikiManifestV1(options) {
    const { index_hash, git_commit, pages, communities, oldManifest, degradedReasons } = options;
    const file_to_community = buildFileToCommunityMap(communities);
    const token_estimates = {};
    const CHARS_PER_TOKEN = 4;
    for (const p of pages)
        token_estimates[p.slug] = Math.ceil(p.content.length / CHARS_PER_TOKEN);
    const slug_redirects = { ...(oldManifest?.slug_redirects ?? {}) };
    const linkRe = /\[\[([^\]]+)\]\]/g;
    const builtPages = pages.map((p) => {
        const outbound_links = [];
        linkRe.lastIndex = 0;
        let m;
        while ((m = linkRe.exec(p.content)) !== null) {
            if (m[1] !== undefined)
                outbound_links.push(m[1]);
        }
        return { slug: p.slug, title: p.title, type: p.type, file: p.file, outbound_links };
    });
    const degraded = (degradedReasons?.length ?? 0) > 0;
    return {
        manifest_schema_version: "2.1.0",
        generated_at: new Date().toISOString(),
        index_hash,
        git_commit,
        pages: builtPages,
        slug_redirects,
        token_estimates,
        file_to_community,
        degraded,
        ...(degraded ? { degraded_reasons: degradedReasons } : {}),
    };
}
//# sourceMappingURL=wiki-module-builder.js.map