import * as fs from "node:fs";
import * as path from "node:path";
import { resolveDependencyPackageRoot } from "./package-roots.js";
import { collectProjectIncludedDeclarationFiles, createCompilerOptions, scanForDeclarationFiles, } from "./core-declarations.js";
import { discoverDeclarationModuleAliases, } from "./declaration-module-aliases.js";
import { readPackageName } from "./module-resolution.js";
import { readSourcePackageMetadata } from "./source-package-metadata.js";
const installedSourcePackageIndexCache = new Map();
const canonicalizeRootDirPath = (filePath) => {
    const normalizedPath = path.resolve(filePath);
    try {
        return fs.realpathSync(normalizedPath);
    }
    catch {
        return normalizedPath;
    }
};
const resolveCommonRootDir = (paths) => {
    const [first, ...remaining] = paths;
    if (!first) {
        throw new Error("resolveCommonRootDir requires at least one path");
    }
    const rest = remaining.map(canonicalizeRootDirPath);
    let current = canonicalizeRootDirPath(first);
    for (;;) {
        const containsAll = rest.every((candidate) => {
            const relative = path.relative(current, candidate);
            return (relative === "" ||
                (!relative.startsWith("..") && !path.isAbsolute(relative)));
        });
        if (containsAll) {
            return current;
        }
        const parent = path.dirname(current);
        if (parent === current) {
            return current;
        }
        current = parent;
    }
};
const readSourcePackageAmbientPaths = (packageRoot) => {
    return readSourcePackageMetadata(packageRoot)?.ambientPaths ?? [];
};
const readSourcePackageExportPaths = (packageRoot) => {
    return readSourcePackageMetadata(packageRoot)?.exportPaths ?? [];
};
const collectSourcePackageModuleAliases = (packageRoots) => {
    const aliases = new Map();
    for (const packageRoot of packageRoots) {
        const metadata = readSourcePackageMetadata(packageRoot);
        if (!metadata) {
            continue;
        }
        for (const [specifier, targetSpecifier] of Object.entries(metadata.moduleAliases)) {
            if (aliases.has(specifier)) {
                continue;
            }
            aliases.set(specifier, {
                targetSpecifier,
                declarationFile: path.join(metadata.packageRoot, "tsonic.package.json"),
            });
        }
    }
    return aliases;
};
const readTsonicDependencyNames = (packageRoot) => {
    const packageJsonPath = path.join(packageRoot, "package.json");
    if (!fs.existsSync(packageJsonPath)) {
        return [];
    }
    try {
        const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
        return Array.from(new Set([
            ...Object.keys(parsed.dependencies ?? {}),
            ...Object.keys(parsed.peerDependencies ?? {}),
            ...Object.keys(parsed.optionalDependencies ?? {}),
        ].filter((packageName) => packageName.length > 0)));
    }
    catch {
        return [];
    }
};
const getSurfacePackageName = (mode) => {
    const trimmed = mode.trim();
    if (trimmed.length === 0 || trimmed === "clr") {
        return undefined;
    }
    return trimmed;
};
const isExplicitAuthoritativePackageRoot = (packageRoot) => {
    if (readSourcePackageMetadata(packageRoot)) {
        return true;
    }
    return (fs.existsSync(path.join(packageRoot, "tsonic.surface.json")) ||
        fs.existsSync(path.join(packageRoot, "tsonic.bindings.json")));
};
const addInstalledSourcePackageCandidate = (packageIndex, packageRoot) => {
    if (!readSourcePackageMetadata(packageRoot)) {
        return;
    }
    const packageName = readPackageName(path.join(packageRoot, "package.json"));
    if (!packageName || packageIndex.has(packageName)) {
        return;
    }
    packageIndex.set(packageName, packageRoot);
};
const isDirectoryLikeEntry = (rootPath, entry) => {
    if (entry.isDirectory()) {
        return true;
    }
    if (!entry.isSymbolicLink()) {
        return false;
    }
    try {
        return fs.statSync(path.join(rootPath, entry.name)).isDirectory();
    }
    catch {
        return false;
    }
};
const scanInstalledSourcePackages = (projectRoot) => {
    const normalizedRoot = path.resolve(projectRoot);
    const cached = installedSourcePackageIndexCache.get(normalizedRoot);
    if (cached !== undefined) {
        return cached;
    }
    const packageIndex = new Map();
    let currentDir = normalizedRoot;
    const workspaceBoundary = (() => {
        let candidate = normalizedRoot;
        for (;;) {
            if (fs.existsSync(path.join(candidate, "tsonic.workspace.json"))) {
                return canonicalizeRootDirPath(candidate);
            }
            const parentDir = path.dirname(candidate);
            if (parentDir === candidate) {
                return undefined;
            }
            candidate = parentDir;
        }
    })();
    for (;;) {
        const nodeModulesRoot = path.join(currentDir, "node_modules");
        if (fs.existsSync(nodeModulesRoot)) {
            const entries = fs.readdirSync(nodeModulesRoot, { withFileTypes: true });
            for (const entry of entries) {
                if (!isDirectoryLikeEntry(nodeModulesRoot, entry)) {
                    continue;
                }
                if (entry.name.startsWith("@")) {
                    const scopeRoot = path.join(nodeModulesRoot, entry.name);
                    const scopedEntries = fs.readdirSync(scopeRoot, {
                        withFileTypes: true,
                    });
                    for (const scopedEntry of scopedEntries) {
                        if (!isDirectoryLikeEntry(scopeRoot, scopedEntry)) {
                            continue;
                        }
                        addInstalledSourcePackageCandidate(packageIndex, path.join(scopeRoot, scopedEntry.name));
                    }
                    continue;
                }
                addInstalledSourcePackageCandidate(packageIndex, path.join(nodeModulesRoot, entry.name));
            }
        }
        if (workspaceBoundary !== undefined &&
            canonicalizeRootDirPath(currentDir) === workspaceBoundary) {
            break;
        }
        const parentDir = path.dirname(currentDir);
        if (parentDir === currentDir) {
            break;
        }
        currentDir = parentDir;
    }
    installedSourcePackageIndexCache.set(normalizedRoot, packageIndex);
    return packageIndex;
};
export const discoverProgramInputs = (filePaths, options, surfaceCapabilities) => {
    const absolutePaths = filePaths.map((filePath) => path.resolve(filePath));
    const userTypeRoots = options.typeRoots ?? [];
    const requestedTypeRoots = Array.from(new Set([
        ...userTypeRoots,
        ...surfaceCapabilities.requiredTypeRoots,
    ]));
    const resolvedRequestedTypeRoots = requestedTypeRoots.map((typeRoot) => {
        const absoluteRoot = path.isAbsolute(typeRoot)
            ? typeRoot
            : path.resolve(options.projectRoot, typeRoot);
        const match = typeRoot.match(/(?:^|[/\\\\])node_modules[/\\\\]@tsonic[/\\\\]([^/\\\\]+)[/\\\\]?$/);
        if (match) {
            const pkgDirName = match[1];
            if (pkgDirName) {
                if (fs.existsSync(absoluteRoot)) {
                    return canonicalizeRootDirPath(absoluteRoot);
                }
                const projectOwned = resolveDependencyPackageRoot(options.projectRoot, `@tsonic/${pkgDirName}`, "installed-first");
                if (projectOwned)
                    return projectOwned;
            }
        }
        if (fs.existsSync(absoluteRoot)) {
            return canonicalizeRootDirPath(absoluteRoot);
        }
        return absoluteRoot;
    });
    const authoritativeTsonicPackageRoots = new Map();
    const activeAuthoritativeSourcePackageRoots = new Map();
    const activeSurfacePackageNames = new Set(surfaceCapabilities.resolvedModes
        .map((mode) => getSurfacePackageName(mode))
        .filter((mode) => mode !== undefined));
    const currentProjectPackageName = readPackageName(path.join(options.projectRoot, "package.json"));
    const currentProjectSourceMetadata = readSourcePackageMetadata(options.projectRoot);
    if (currentProjectPackageName && currentProjectSourceMetadata) {
        const normalizedProjectRoot = canonicalizeRootDirPath(options.projectRoot);
        authoritativeTsonicPackageRoots.set(currentProjectPackageName, normalizedProjectRoot);
        activeAuthoritativeSourcePackageRoots.set(currentProjectPackageName, normalizedProjectRoot);
    }
    for (const typeRoot of resolvedRequestedTypeRoots) {
        const packageName = readPackageName(path.join(typeRoot, "package.json"));
        if (packageName && isExplicitAuthoritativePackageRoot(typeRoot)) {
            authoritativeTsonicPackageRoots.set(packageName, typeRoot);
            if (readSourcePackageMetadata(typeRoot)) {
                activeAuthoritativeSourcePackageRoots.set(packageName, typeRoot);
            }
        }
    }
    for (const [packageName, packageRoot] of scanInstalledSourcePackages(options.projectRoot)) {
        if (!authoritativeTsonicPackageRoots.has(packageName)) {
            authoritativeTsonicPackageRoots.set(packageName, packageRoot);
        }
        if (activeSurfacePackageNames.has(packageName) &&
            !activeAuthoritativeSourcePackageRoots.has(packageName)) {
            activeAuthoritativeSourcePackageRoots.set(packageName, packageRoot);
        }
    }
    const waveQueue = Array.from(activeAuthoritativeSourcePackageRoots.entries());
    const visitedWaveRoots = new Set();
    while (waveQueue.length > 0) {
        const nextWaveEntry = waveQueue.shift();
        if (!nextWaveEntry) {
            continue;
        }
        const [packageName, packageRoot] = nextWaveEntry;
        const visitKey = `${packageName}::${path.resolve(packageRoot)}`;
        if (visitedWaveRoots.has(visitKey)) {
            continue;
        }
        visitedWaveRoots.add(visitKey);
        for (const dependencyName of readTsonicDependencyNames(packageRoot)) {
            const dependencyRoot = resolveDependencyPackageRoot(packageRoot, dependencyName, "sibling-first");
            if (!dependencyRoot) {
                continue;
            }
            const existingRoot = authoritativeTsonicPackageRoots.get(dependencyName);
            const existingIsSourcePackage = existingRoot !== undefined && readSourcePackageMetadata(existingRoot);
            const dependencyIsSourcePackage = readSourcePackageMetadata(dependencyRoot);
            if (existingRoot !== undefined &&
                existingRoot !== dependencyRoot &&
                existingIsSourcePackage &&
                !dependencyIsSourcePackage) {
                continue;
            }
            if (existingRoot !== dependencyRoot) {
                authoritativeTsonicPackageRoots.set(dependencyName, dependencyRoot);
            }
            if (dependencyIsSourcePackage &&
                activeAuthoritativeSourcePackageRoots.get(dependencyName) !==
                    dependencyRoot) {
                activeAuthoritativeSourcePackageRoots.set(dependencyName, dependencyRoot);
                waveQueue.push([dependencyName, dependencyRoot]);
            }
        }
    }
    const nonAuthoritativeTypeRoots = resolvedRequestedTypeRoots.filter((typeRoot) => {
        const packageName = readPackageName(path.join(typeRoot, "package.json"));
        return !(packageName && readSourcePackageMetadata(typeRoot));
    });
    const orderedSurfaceTypeRoots = [];
    const seenSurfaceTypeRoots = new Set();
    const pushSurfaceTypeRoot = (root) => {
        if (!root || seenSurfaceTypeRoots.has(root)) {
            return;
        }
        seenSurfaceTypeRoots.add(root);
        orderedSurfaceTypeRoots.push(root);
    };
    for (const mode of [...surfaceCapabilities.resolvedModes].reverse()) {
        const packageName = getSurfacePackageName(mode);
        if (!packageName) {
            continue;
        }
        pushSurfaceTypeRoot(authoritativeTsonicPackageRoots.get(packageName) ??
            activeAuthoritativeSourcePackageRoots.get(packageName));
    }
    for (const root of activeAuthoritativeSourcePackageRoots.values()) {
        pushSurfaceTypeRoot(root);
    }
    for (const root of nonAuthoritativeTypeRoots) {
        pushSurfaceTypeRoot(root);
    }
    const typeRoots = [...orderedSurfaceTypeRoots];
    const sourcePackageAmbientPaths = typeRoots.flatMap((typeRoot) => readSourcePackageAmbientPaths(typeRoot));
    const sourcePackageExportPaths = currentProjectSourceMetadata
        ? readSourcePackageExportPaths(currentProjectSourceMetadata.packageRoot)
        : [];
    if (options.verbose && typeRoots.length > 0) {
        console.log(`TypeRoots: ${typeRoots.join(", ")}`);
    }
    const declarationFiles = [];
    for (const typeRoot of nonAuthoritativeTypeRoots) {
        declarationFiles.push(...scanForDeclarationFiles(path.resolve(typeRoot)));
    }
    const namespaceIndexFiles = [];
    for (const typeRoot of nonAuthoritativeTypeRoots) {
        const absoluteRoot = path.resolve(typeRoot);
        if (options.verbose) {
            console.log(`Checking typeRoot: ${absoluteRoot}, exists: ${fs.existsSync(absoluteRoot)}`);
        }
        if (fs.existsSync(absoluteRoot)) {
            const entries = fs.readdirSync(absoluteRoot, { withFileTypes: true });
            for (const entry of entries) {
                if (entry.isDirectory() &&
                    !entry.name.startsWith("_") &&
                    !entry.name.startsWith("internal")) {
                    const indexPath = path.join(absoluteRoot, entry.name, "index.d.ts");
                    if (fs.existsSync(indexPath)) {
                        namespaceIndexFiles.push(indexPath);
                        if (options.verbose) {
                            console.log(`  Found namespace: ${entry.name} -> ${indexPath}`);
                        }
                    }
                }
            }
        }
    }
    const tsOptions = createCompilerOptions(options);
    if (typeof tsOptions.rootDir === "string" && absolutePaths.length > 0) {
        tsOptions.rootDir = resolveCommonRootDir([
            tsOptions.rootDir,
            ...absolutePaths,
            ...sourcePackageAmbientPaths,
            ...activeAuthoritativeSourcePackageRoots.values(),
        ]);
    }
    const projectDeclarationFiles = collectProjectIncludedDeclarationFiles(options.projectRoot, tsOptions);
    const declarationModuleAliases = new Map(collectSourcePackageModuleAliases([
        ...typeRoots,
        ...authoritativeTsonicPackageRoots.values(),
    ]));
    for (const [specifier, alias] of discoverDeclarationModuleAliases([
        ...projectDeclarationFiles,
        ...declarationFiles,
        ...sourcePackageAmbientPaths,
        ...namespaceIndexFiles,
    ])) {
        if (!declarationModuleAliases.has(specifier)) {
            declarationModuleAliases.set(specifier, alias);
        }
    }
    const allFiles = Array.from(new Set([
        ...absolutePaths,
        ...sourcePackageAmbientPaths,
        ...sourcePackageExportPaths,
        ...projectDeclarationFiles,
        ...declarationFiles,
        ...namespaceIndexFiles,
    ]));
    return {
        absolutePaths,
        typeRoots,
        authoritativeTsonicPackageRoots,
        namespaceIndexFiles,
        declarationModuleAliases,
        allFiles,
        tsOptions,
    };
};
//# sourceMappingURL=program-input-discovery.js.map