/**
 * DtsPackageSurface — `PackageSurfaceResolver` adapter that reads installed
 * package `.d.ts` files from `node_modules`.
 *
 * Resolution flow:
 *   1. Find the package's root directory via the configured resolver
 *      (default uses `createRequire` against the working directory).
 *   2. Read its `package.json` to capture the resolved `version` and the
 *      `.d.ts` entry path (`types`, `typings`, or `exports["."].types`).
 *   3. Parse the entry `.d.ts` for top-level export declarations.
 *   4. Follow ONE hop of `export * from "./relative"` re-exports — direct
 *      siblings only, no transitive walking.
 *   5. Cache the result by package name for the resolver's lifetime.
 *
 * Throws `PackageSurfaceResolverError` with a typed `reason` when the
 * package isn't installed or its types entry can't be resolved. Callers
 * (the W0198 preflight) catch the error and convert it into per-binding
 * `unresolved` findings rather than `missing` deductions.
 */
import { existsSync, readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { dirname, isAbsolute, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { PackageSurfaceResolverError, } from "../../_vendor/ailf-core/index.js";
import { parseDtsExports } from "./parse-dts-exports.js";
export class DtsPackageSurface {
    resolveRoot;
    cache = new Map();
    constructor(opts = {}) {
        this.resolveRoot =
            opts.resolvePackageRoot ??
                makeDefaultPackageRootResolver(opts.resolveFromDir ?? process.cwd());
    }
    async resolveExports(pkg) {
        const cached = this.cache.get(pkg);
        if (cached)
            return cached;
        const root = this.resolveRoot(pkg);
        if (!root) {
            throw new PackageSurfaceResolverError("package-not-installed", pkg, `Package "${pkg}" was not resolvable from the configured lookup path.`);
        }
        const pkgJsonPath = join(root, "package.json");
        if (!existsSync(pkgJsonPath)) {
            throw new PackageSurfaceResolverError("package-not-installed", pkg, `Package "${pkg}" has no package.json at "${pkgJsonPath}".`);
        }
        const pkgJson = readPackageJson(pkgJsonPath, pkg);
        const version = pkgJson.version ?? "0.0.0";
        const typesEntry = resolveTypesEntry(pkgJson, root);
        if (!typesEntry) {
            throw new PackageSurfaceResolverError("types-entry-missing", pkg, `Package "${pkg}@${version}" does not declare a \`types\` entry ` +
                `(checked package.json \`types\`, \`typings\`, and \`exports["."].types\`).`);
        }
        const symbols = readSurface(pkg, version, typesEntry);
        const surface = {
            pkg,
            version,
            symbols,
        };
        this.cache.set(pkg, surface);
        return surface;
    }
}
function readPackageJson(path, pkg) {
    try {
        return JSON.parse(readFileSync(path, "utf-8"));
    }
    catch (err) {
        throw new PackageSurfaceResolverError("parse-failed", pkg, `Failed to parse "${path}": ${err instanceof Error ? err.message : String(err)}`);
    }
}
/**
 * Pick the `.d.ts` entry the resolver should parse. Order:
 *   1. `package.json#types`
 *   2. `package.json#typings`
 *   3. `exports["."].types` (string or object form)
 */
function resolveTypesEntry(pkgJson, root) {
    const candidates = [];
    if (typeof pkgJson.types === "string")
        candidates.push(pkgJson.types);
    if (typeof pkgJson.typings === "string")
        candidates.push(pkgJson.typings);
    const dotExport = pkgJson.exports && typeof pkgJson.exports === "object"
        ? pkgJson.exports["."]
        : undefined;
    if (typeof dotExport === "string") {
        if (dotExport.endsWith(".d.ts"))
            candidates.push(dotExport);
    }
    else if (dotExport && typeof dotExport === "object") {
        const typesField = dotExport.types;
        if (typeof typesField === "string")
            candidates.push(typesField);
        else if (typesField && typeof typesField === "object") {
            // Conditional `types` entry (rare) — pick any string leaf.
            for (const v of Object.values(typesField)) {
                if (typeof v === "string") {
                    candidates.push(v);
                    break;
                }
            }
        }
    }
    for (const candidate of candidates) {
        const abs = isAbsolute(candidate) ? candidate : join(root, candidate);
        if (existsSync(abs))
            return abs;
    }
    return null;
}
function readSurface(pkg, version, entryPath) {
    const names = new Set();
    const visited = new Set();
    const parseFile = (path, hops) => {
        if (visited.has(path))
            return;
        visited.add(path);
        let src;
        try {
            src = readFileSync(path, "utf-8");
        }
        catch (err) {
            throw new PackageSurfaceResolverError("parse-failed", pkg, `Failed to read "${path}" for "${pkg}@${version}": ${err instanceof Error ? err.message : String(err)}`);
        }
        const parsed = parseDtsExports(src);
        for (const name of parsed.names)
            names.add(name);
        if (hops <= 0)
            return;
        const baseDir = dirname(path);
        for (const spec of parsed.reExports) {
            if (!spec.startsWith("."))
                continue; // bare specifier — out of scope
            const resolved = resolveRelativeDts(baseDir, spec);
            if (resolved)
                parseFile(resolved, hops - 1);
        }
    };
    parseFile(entryPath, /* hops */ 1);
    return [...names].sort().map((name) => ({ name, source: "types" }));
}
/**
 * Resolve a relative re-export specifier to an existing `.d.ts` file.
 * Tries `<spec>.d.ts`, `<spec>/index.d.ts`, and `<spec>` literally.
 */
function resolveRelativeDts(baseDir, spec) {
    const base = resolve(baseDir, spec);
    const candidates = [
        base.endsWith(".d.ts") ? base : null,
        `${base}.d.ts`,
        join(base, "index.d.ts"),
    ].filter((p) => p !== null);
    for (const path of candidates) {
        if (existsSync(path))
            return path;
    }
    return null;
}
function makeDefaultPackageRootResolver(fromDir) {
    // `createRequire` needs a file URL or path that ends with a slash so
    // it knows it's a directory, not a file.
    const anchor = fromDir.endsWith("/") ? fromDir : `${fromDir}/`;
    const req = createRequire(pathToFileURL(anchor));
    return (pkg) => {
        try {
            const pkgJsonPath = req.resolve(`${pkg}/package.json`);
            return dirname(pkgJsonPath);
        }
        catch {
            return null;
        }
    };
}
