/**
 * Hilt DI graph tools.
 *
 * Build a dependency graph from indexed Kotlin symbols by scanning for Hilt
 * annotations (@HiltViewModel, @AndroidEntryPoint, @Module, @Provides, @Binds)
 * and matching each @Inject constructor parameter type against a provider
 * method that returns that type.
 *
 * Index-only (no filesystem rescan) — operates on whatever the Kotlin
 * extractor already surfaced. Requires the extractor to populate
 * `decorators` on class/method symbols (added in Wave 2 Task 3).
 */
import { getCodeIndex } from "./index-tools.js";
// ---------------------------------------------------------------------------
// Annotation helpers
// ---------------------------------------------------------------------------
const ENTRY_ANNOTATIONS = {
    HiltViewModel: "HiltViewModel",
    HiltAndroidApp: "HiltAndroidApp",
    AndroidEntryPoint: "AndroidEntryPoint",
};
function hasAnnotation(sym, name) {
    if (sym.decorators && sym.decorators.includes(name))
        return true;
    // Fallback: scan source header for @Annotation (word-bounded so
    // @HiltViewModelExtra does not match @HiltViewModel).
    const head = sym.source?.slice(0, 400);
    if (!head)
        return false;
    return new RegExp(`@${name}\\b`).test(head);
}
function firstMatchingAnnotation(sym, annotations) {
    for (const [name, kind] of Object.entries(annotations)) {
        if (hasAnnotation(sym, name))
            return kind;
    }
    return null;
}
// ---------------------------------------------------------------------------
// Dependency parsing
// ---------------------------------------------------------------------------
/**
 * Parse constructor parameter types from the source of a Hilt entry-point
 * class. Matches `@Inject constructor(...)` and extracts the type of each
 * parameter.
 *
 * Handles:
 *   - val/var prefix, private modifier
 *   - nullable types (String?)
 *   - generic types (List<User>, Map<K, V>)
 *   - assisted inject parameters (for now just returns all param types)
 */
function parseInjectedDependencies(sym) {
    const src = sym.source;
    if (!src)
        return [];
    // Match `@Inject constructor(PARAMS)` — params can span multiple lines.
    const match = /@Inject\s+constructor\s*\(([\s\S]*?)\)/.exec(src);
    if (!match)
        return [];
    const paramList = match[1].trim();
    if (!paramList)
        return [];
    // Split on top-level commas only (don't split inside generic brackets).
    const params = splitTopLevelCommas(paramList);
    const types = [];
    for (const raw of params) {
        const param = raw.trim();
        if (!param)
            continue;
        // Strip modifiers: private, val, var, @Assisted, @Named("x"), etc.
        const withoutModifiers = param
            .replace(/@\w+(?:\([^)]*\))?\s+/g, "")
            .replace(/\b(?:private|public|internal|protected|val|var)\s+/g, "");
        // Expect `name: Type` — split on the first colon.
        const colonIdx = withoutModifiers.indexOf(":");
        if (colonIdx === -1)
            continue;
        const typeText = withoutModifiers.slice(colonIdx + 1).trim();
        // Strip trailing `?` for nullable, default value ` = ...`, and
        // optional trailing comma.
        const cleanType = typeText
            .replace(/\s*=\s*[\s\S]*$/, "") // drop default values
            .replace(/,$/, "")
            .trim();
        // Use the root type name (strip generic parameters for matching).
        const rootType = cleanType.replace(/<[\s\S]*$/, "").replace(/\?$/, "").trim();
        if (rootType)
            types.push(rootType);
    }
    return types;
}
function splitTopLevelCommas(text) {
    const parts = [];
    let depth = 0;
    let start = 0;
    for (let i = 0; i < text.length; i++) {
        const ch = text[i];
        if (ch === "<" || ch === "(" || ch === "[")
            depth++;
        else if (ch === ">" || ch === ")" || ch === "]")
            depth--;
        else if (ch === "," && depth === 0) {
            parts.push(text.slice(start, i));
            start = i + 1;
        }
    }
    parts.push(text.slice(start));
    return parts;
}
// ---------------------------------------------------------------------------
// Provider parsing
// ---------------------------------------------------------------------------
/**
 * Extract the return type from a provider method signature.
 *
 *   "(): UserRepository"                     → "UserRepository"
 *   "(impl: UserRepositoryImpl): UserRepository" → "UserRepository"
 *   "(): List<User>"                         → "List"
 */
function parseProviderReturnType(sym) {
    const sig = sym.signature;
    if (!sig)
        return null;
    // The return type sits after the last `):`.
    const idx = sig.lastIndexOf("):");
    if (idx === -1)
        return null;
    const rawReturn = sig.slice(idx + 2).trim();
    const rootType = rawReturn.replace(/<[\s\S]*$/, "").replace(/\?$/, "").trim();
    return rootType || null;
}
// ---------------------------------------------------------------------------
// Graph builder
// ---------------------------------------------------------------------------
export async function buildHiltGraph(repo) {
    const index = await getCodeIndex(repo);
    if (!index) {
        throw new Error(`Repository "${repo}" not found. Index it first with index_folder.`);
    }
    const viewModels = [];
    const entryPoints = [];
    const modules = [];
    // First pass — classify classes as entry points or modules.
    // Track modules by id so we can attach providers in the second pass.
    const modulesById = new Map();
    for (const sym of index.symbols) {
        if (sym.kind !== "class" && sym.kind !== "interface")
            continue;
        const entryKind = firstMatchingAnnotation(sym, ENTRY_ANNOTATIONS);
        if (entryKind) {
            const entry = {
                name: sym.name,
                file: sym.file,
                start_line: sym.start_line,
                kind: entryKind,
                dependencies: parseInjectedDependencies(sym),
            };
            if (entryKind === "HiltViewModel") {
                viewModels.push(entry);
            }
            else {
                entryPoints.push(entry);
            }
            continue;
        }
        if (hasAnnotation(sym, "Module")) {
            const mod = {
                name: sym.name,
                file: sym.file,
                start_line: sym.start_line,
                providers: [],
            };
            modules.push(mod);
            modulesById.set(sym.id, mod);
        }
    }
    // Second pass — attach provider methods to their parent modules.
    for (const sym of index.symbols) {
        if (sym.kind !== "method" && sym.kind !== "function")
            continue;
        if (!sym.parent)
            continue;
        const module = modulesById.get(sym.parent);
        if (!module)
            continue;
        let providerKind = null;
        if (hasAnnotation(sym, "Provides"))
            providerKind = "provides";
        else if (hasAnnotation(sym, "Binds"))
            providerKind = "binds";
        if (!providerKind)
            continue;
        const returnType = parseProviderReturnType(sym);
        if (!returnType)
            continue;
        module.providers.push({
            name: sym.name,
            file: sym.file,
            start_line: sym.start_line,
            kind: providerKind,
            provides: returnType,
        });
    }
    // Build edge table — map provider type → {method, module}.
    const providerIndex = new Map();
    for (const module of modules) {
        for (const provider of module.providers) {
            if (!providerIndex.has(provider.provides)) {
                providerIndex.set(provider.provides, { provider, module });
            }
        }
    }
    const edges = [];
    for (const entry of [...viewModels, ...entryPoints]) {
        for (const dep of entry.dependencies) {
            const provider = providerIndex.get(dep);
            const edge = { from: entry.name, to: dep };
            if (provider) {
                edge.provided_by = provider.provider.name;
                edge.module = provider.module.name;
            }
            edges.push(edge);
        }
    }
    return { view_models: viewModels, entry_points: entryPoints, modules, edges };
}
export async function traceHiltGraph(repo, className, options) {
    const graph = await buildHiltGraph(repo);
    const depth = options?.depth ?? 1;
    const root = graph.view_models.find((v) => v.name === className) ??
        graph.entry_points.find((e) => e.name === className);
    if (!root) {
        throw new Error(`"${className}" is not a Hilt entry point (missing @HiltViewModel / @HiltAndroidApp / @AndroidEntryPoint annotation).`);
    }
    const dependencies = root.dependencies.map((dep) => {
        const edge = graph.edges.find((e) => e.from === className && e.to === dep);
        const node = { name: dep };
        if (edge?.provided_by)
            node.provided_by = edge.provided_by;
        if (edge?.module)
            node.module = edge.module;
        if (!edge?.provided_by)
            node.unresolved = true;
        return node;
    });
    return {
        root: {
            name: root.name,
            kind: root.kind,
            file: root.file,
            start_line: root.start_line,
        },
        dependencies,
        depth,
    };
}
//# sourceMappingURL=hilt-tools.js.map