import fs from "node:fs";
import path from "node:path";
import { ensureTrailingSlash, hasGlobPattern, isPathInScope, isSameOrDescendantPath, matchesScopePattern, normalizeScopePattern, toPosixPath, } from "../core/path_scope_utils.js";
const IGNORED_DIRS = new Set([
    ".claude",
    ".codex",
    ".git",
    ".soloforge",
    ".trae",
    "build",
    "coverage",
    "dist",
    "node_modules",
    "out",
    "target",
]);
const BACKEND_HINTS = [
    "backend",
    "server",
    "service",
    "services",
    "api",
    "web",
    "model",
    "dao",
    "repository",
];
const FRONTEND_HINTS = [
    "frontend",
    "client",
    "webapp",
    "ui",
    "portal",
    "platform",
    "institution",
];
const KNOWN_SOURCE_ROOT_PATTERN = /^(?:src|[^/]+\/src|[^/]+\/[^/]+\/src|[^/]+\/[^/]+\/[^/]+\/src)\/$/;
const PATH_CASE_SENSITIVE = process.platform !== "win32" && process.platform !== "darwin";
function pathExists(projectPath, relativePath) {
    try {
        return fs.existsSync(path.join(projectPath, relativePath));
    }
    catch {
        return false;
    }
}
function isDirectory(projectPath, relativePath) {
    try {
        return fs.statSync(path.join(projectPath, relativePath)).isDirectory();
    }
    catch {
        return false;
    }
}
function readDirSafe(projectPath, relativePath) {
    try {
        return fs.readdirSync(path.join(projectPath, relativePath), { withFileTypes: true });
    }
    catch {
        return [];
    }
}
function normalizeRelPath(rawPath, basePath) {
    const normalized = String(rawPath ?? "").replace(/\\/g, "/").trim();
    if (!normalized || normalized === ".")
        return normalizeScopePattern(basePath ?? ".");
    const combined = basePath && basePath !== "."
        ? path.posix.normalize(path.posix.join(toPosixPath(basePath), normalized))
        : path.posix.normalize(normalized);
    return normalizeScopePattern(combined);
}
function dedupePreserveOrder(paths) {
    const seen = new Set();
    const result = [];
    for (const raw of paths) {
        const normalized = normalizeScopePattern(raw);
        if (!normalized || seen.has(normalized))
            continue;
        seen.add(normalized);
        result.push(normalized);
    }
    return result;
}
function removeNestedBroadDuplicates(paths) {
    const sorted = dedupePreserveOrder(paths).sort((a, b) => a.length - b.length);
    const result = [];
    for (const candidate of sorted) {
        if (hasGlobPattern(candidate)) {
            result.push(candidate);
            continue;
        }
        if (KNOWN_SOURCE_ROOT_PATTERN.test(candidate)) {
            for (let i = result.length - 1; i >= 0; i -= 1) {
                const existing = result[i];
                if (!hasGlobPattern(existing)
                    && KNOWN_SOURCE_ROOT_PATTERN.test(existing)
                    && isSameOrDescendantPath(candidate, existing, { caseSensitive: PATH_CASE_SENSITIVE })) {
                    result.splice(i, 1);
                }
            }
        }
        if (result.some((existing) => !hasGlobPattern(existing)
            && isSameOrDescendantPath(candidate, existing, { caseSensitive: PATH_CASE_SENSITIVE }))) {
            continue;
        }
        result.push(candidate);
    }
    return result;
}
function getAffectedRepos(classification) {
    return Array.isArray(classification?.affected_repos)
        ? classification.affected_repos.filter((repo) => typeof repo === "string" && repo.length > 0)
        : [];
}
function taskNeedsBackend(input) {
    const affected = getAffectedRepos(input.classification).join(" ").toLowerCase();
    const intent = `${input.classification.task_type} ${input.routeDecision?.workflow_intent ?? ""} ${affected}`;
    if (/backend|server|api|java|spring|数据库|接口|后端|服务|mapper|controller|service/i.test(intent))
        return true;
    if (input.config.tech_stack.backend?.framework && input.config.tech_stack.backend.framework !== "unknown") {
        return affected.length === 0 || /backend|server|api|service|web|model/.test(affected);
    }
    return false;
}
function taskNeedsFrontend(input) {
    const affected = getAffectedRepos(input.classification).join(" ").toLowerCase();
    const intent = `${input.classification.task_type} ${input.routeDecision?.workflow_intent ?? ""} ${affected}`;
    if (/frontend|client|react|vue|页面|前端|component|route|ui/i.test(intent))
        return true;
    if (input.config.tech_stack.frontend?.framework && input.config.tech_stack.frontend.framework !== "unknown") {
        return affected.length === 0 || /frontend|client|ui|app|web|platform|institution/.test(affected);
    }
    return false;
}
function roleFromPath(relativePath) {
    const lower = relativePath.toLowerCase();
    if (FRONTEND_HINTS.some((hint) => lower.split("/").includes(hint) || lower.includes(`/${hint}/`)))
        return "frontend";
    if (BACKEND_HINTS.some((hint) => lower.split("/").includes(hint) || lower.includes(`/${hint}/`)))
        return "backend";
    if (/(?:^|\/)src\/main\/(?:java|kotlin|scala)(?:\/|$)/.test(lower))
        return "backend";
    if (/(?:^|\/)(?:src|app)\/(?:pages|components|views|router|routes)(?:\/|$)/.test(lower))
        return "frontend";
    return "shared";
}
function addSourceRoot(roots, root) {
    const normalized = ensureTrailingSlash(root.path);
    if (!normalized || normalized === "./")
        return;
    const existing = roots.get(normalized);
    if (existing) {
        existing.evidence.push(...root.evidence.filter((item) => !existing.evidence.includes(item)));
        if (existing.role === "shared" && root.role !== "shared")
            existing.role = root.role;
        return;
    }
    roots.set(normalized, { ...root, path: normalized });
}
function discoverMavenRoots(projectPath, roots) {
    const stack = [""];
    while (stack.length > 0) {
        const current = stack.pop();
        const entries = readDirSafe(projectPath, current);
        for (const entry of entries) {
            if (!entry.isDirectory() || IGNORED_DIRS.has(entry.name))
                continue;
            const child = current ? `${current}/${entry.name}` : entry.name;
            const depth = child.split("/").length;
            if (depth <= 4)
                stack.push(child);
        }
        const hasJavaSource = isDirectory(projectPath, path.posix.join(current, "src/main/java"))
            || isDirectory(projectPath, path.posix.join(current, "src/main/kotlin"));
        const hasBuildFile = pathExists(projectPath, path.posix.join(current, "pom.xml"))
            || pathExists(projectPath, path.posix.join(current, "build.gradle"))
            || pathExists(projectPath, path.posix.join(current, "build.gradle.kts"));
        const currentIsSourceLayer = /(?:^|\/)src(?:\/main)?$/i.test(current);
        if (!currentIsSourceLayer && (hasJavaSource || (hasBuildFile && isDirectory(projectPath, path.posix.join(current, "src"))))) {
            addSourceRoot(roots, {
                path: current ? `${current}/src/` : "src/",
                role: "backend",
                evidence: [
                    hasJavaSource ? "detected src/main Java/Kotlin source tree" : "detected JVM build file with src directory",
                ],
            });
        }
    }
}
function discoverFrontendRoots(projectPath, roots) {
    const containerNames = new Set(["apps", "packages", "modules", "services", "clients", "frontends", "workspaces"]);
    const stack = [""];
    while (stack.length > 0) {
        const current = stack.pop();
        for (const entry of readDirSafe(projectPath, current)) {
            if (!entry.isDirectory() || IGNORED_DIRS.has(entry.name))
                continue;
            const child = current ? `${current}/${entry.name}` : entry.name;
            const depth = child.split("/").length;
            if (depth <= 3)
                stack.push(child);
        }
        const srcCandidate = current ? `${current}/src` : "src";
        if (isDirectory(projectPath, srcCandidate)) {
            addSourceRoot(roots, {
                path: `${srcCandidate}/`,
                role: roleFromPath(srcCandidate),
                evidence: ["detected frontend source directory candidate"],
            });
        }
        if (containerNames.has(path.posix.basename(current))) {
            for (const entry of readDirSafe(projectPath, current)) {
                if (!entry.isDirectory() || IGNORED_DIRS.has(entry.name))
                    continue;
                const src = current ? `${current}/${entry.name}/src` : `${entry.name}/src`;
                if (!isDirectory(projectPath, src))
                    continue;
                addSourceRoot(roots, {
                    path: `${src}/`,
                    role: "frontend",
                    evidence: [`detected workspace package source: ${current || "."}/*/src`],
                });
            }
        }
    }
}
function discoverSourceRoots(projectPath) {
    const roots = new Map();
    discoverMavenRoots(projectPath, roots);
    discoverFrontendRoots(projectPath, roots);
    for (const topLevel of readDirSafe(projectPath, "")) {
        if (!topLevel.isDirectory() || IGNORED_DIRS.has(topLevel.name))
            continue;
        const directSrc = `${topLevel.name}/src`;
        if (isDirectory(projectPath, directSrc)) {
            addSourceRoot(roots, {
                path: `${directSrc}/`,
                role: roleFromPath(directSrc),
                evidence: ["detected top-level module src directory"],
            });
        }
    }
    return [...roots.values()].sort((a, b) => a.path.localeCompare(b.path));
}
function expandWeakPath(projectPath, declaredPath, role, discoveredRoots, warnings) {
    const normalized = normalizeRelPath(declaredPath);
    if (pathExists(projectPath, normalized))
        return [normalized];
    const basename = normalized.replace(/\/$/, "");
    if (basename === "src" || basename.endsWith("/src")) {
        const matchingRoots = discoveredRoots
            .filter((root) => role === "shared" || root.role === role || root.role === "shared")
            .map((root) => root.path);
        if (matchingRoots.length > 0) {
            warnings.push(`scope ${declaredPath} 在项目根不存在，已按真实多模块源码树展开为 ${matchingRoots.join(", ")}`);
            return matchingRoots;
        }
    }
    warnings.push(`配置 scope ${declaredPath} 在项目中不存在，保留为声明路径但标记为低置信`);
    return [normalized];
}
function addCandidate(candidates, candidate) {
    for (const resolved of candidate.evidence.length > 0 ? [candidate] : [candidate]) {
        candidates.push({
            ...resolved,
            path: normalizeScopePattern(resolved.path),
        });
    }
}
function addConfigScopeCandidates(input, discoveredRoots, candidates, warnings) {
    const needsBackend = taskNeedsBackend(input);
    const needsFrontend = taskNeedsFrontend(input);
    if (needsBackend) {
        for (const declared of input.config.scope?.backend ?? []) {
            for (const expanded of expandWeakPath(input.projectPath, declared, "backend", discoveredRoots, warnings)) {
                addCandidate(candidates, {
                    path: expanded,
                    source: "config.scope.backend",
                    confidence: pathExists(input.projectPath, expanded) ? "high" : "low",
                    role: "backend",
                    declared_path: declared,
                    evidence: ["configured backend scope"],
                });
            }
        }
    }
    if (needsFrontend) {
        for (const declared of input.config.scope?.frontend ?? []) {
            for (const expanded of expandWeakPath(input.projectPath, declared, "frontend", discoveredRoots, warnings)) {
                addCandidate(candidates, {
                    path: expanded,
                    source: "config.scope.frontend",
                    confidence: pathExists(input.projectPath, expanded) ? "high" : "low",
                    role: "frontend",
                    declared_path: declared,
                    evidence: ["configured frontend scope"],
                });
            }
        }
    }
}
function repoMatches(repo, affectedRepos) {
    if (affectedRepos.length === 0)
        return true;
    const name = repo.name.toLowerCase();
    const repoPath = repo.path.toLowerCase();
    return affectedRepos.some((affected) => {
        const normalized = affected.toLowerCase();
        return normalized === name
            || name.includes(normalized)
            || normalized.includes(name)
            || repoPath.includes(normalized);
    });
}
function repoRole(repo) {
    const combined = `${repo.name} ${repo.path} ${repo.lang} ${repo.framework}`.toLowerCase();
    if (/react|vue|angular|frontend|client|ui|typescript/.test(combined))
        return "frontend";
    if (/spring|java|backend|server|api|service|go|python|node/.test(combined))
        return "backend";
    return "shared";
}
function addRepoCandidates(input, discoveredRoots, candidates, warnings) {
    const affectedRepos = getAffectedRepos(input.classification);
    for (const repo of input.config.repos ?? []) {
        if (!repoMatches(repo, affectedRepos))
            continue;
        const role = repoRole(repo);
        for (const declaredScope of repo.scope ?? []) {
            const scopeLooksProjectRelative = declaredScope.replace(/\\/g, "/").startsWith(`${repo.path.replace(/\\/g, "/").replace(/\/$/, "")}/`);
            const declared = scopeLooksProjectRelative
                ? normalizeRelPath(declaredScope)
                : normalizeRelPath(declaredScope, repo.path);
            const expandedPaths = pathExists(input.projectPath, declared)
                ? [declared]
                : expandWeakPath(input.projectPath, declared, role, discoveredRoots, warnings);
            for (const expanded of expandedPaths) {
                addCandidate(candidates, {
                    path: expanded,
                    source: "config.repos.scope",
                    confidence: pathExists(input.projectPath, expanded) ? "high" : "medium",
                    role,
                    repo_name: repo.name,
                    base_path: normalizeRelPath(repo.path),
                    declared_path: declaredScope,
                    evidence: [`repo ${repo.name} scope`],
                });
            }
        }
    }
}
function addRouteCandidates(input, candidates) {
    for (const routePath of input.routeDecision?.scope?.allowed_paths ?? []) {
        addCandidate(candidates, {
            path: normalizeRelPath(routePath),
            source: "route_decision.scope.allowed_paths",
            confidence: "high",
            role: "shared",
            declared_path: routePath,
            evidence: ["explicit route scope"],
        });
    }
}
function addFingerprintCandidates(input, discoveredRoots, candidates) {
    const needsBackend = taskNeedsBackend(input);
    const needsFrontend = taskNeedsFrontend(input);
    for (const root of discoveredRoots) {
        if ((root.role === "backend" && !needsBackend) || (root.role === "frontend" && !needsFrontend))
            continue;
        addCandidate(candidates, {
            path: root.path,
            source: "project_fingerprint.source_roots",
            confidence: "medium",
            role: root.role,
            evidence: root.evidence,
        });
    }
}
function buildSources(candidates, selectedPaths) {
    const grouped = new Map();
    for (const candidate of candidates) {
        if (!selectedPaths.includes(candidate.path))
            continue;
        const key = [
            candidate.source,
            candidate.role,
            candidate.repo_name ?? "",
            candidate.base_path ?? "",
            candidate.declared_path ?? candidate.path,
        ].join("|");
        const current = grouped.get(key);
        if (current) {
            if (!current.resolved_paths.includes(candidate.path))
                current.resolved_paths.push(candidate.path);
            current.evidence.push(...candidate.evidence.filter((item) => !current.evidence.includes(item)));
        }
        else {
            grouped.set(key, {
                source: candidate.source,
                confidence: candidate.confidence,
                role: candidate.role,
                repo_name: candidate.repo_name,
                base_path: candidate.base_path,
                declared_paths: [candidate.declared_path ?? candidate.path],
                resolved_paths: [candidate.path],
                evidence: [...candidate.evidence],
            });
        }
    }
    return [...grouped.values()];
}
function readonlyFromRepos(input) {
    const affected = getAffectedRepos(input.classification);
    const readonly = [];
    for (const repo of input.config.repos ?? []) {
        if (repoMatches(repo, affected))
            continue;
        for (const scope of repo.scope ?? []) {
            readonly.push(normalizeRelPath(scope, repo.path));
        }
    }
    return dedupePreserveOrder(readonly);
}
function isBroadRootOnly(paths) {
    return paths.length === 1 && (paths[0] === "." || paths[0] === "./");
}
function buildReport(input, allowedPaths, readonlyPaths, newFilesAllowed, candidates, discoveredRoots, warnings, conflicts, status) {
    return {
        status,
        project_root: input.projectPath,
        path_semantics: {
            storage_format: "project_relative_posix",
            host_platform: process.platform,
            case_sensitive: process.platform !== "win32" && process.platform !== "darwin",
        },
        allowed_paths: allowedPaths,
        readonly_paths: readonlyPaths,
        new_files_allowed: newFilesAllowed,
        sources: buildSources(candidates, allowedPaths),
        discovered_source_roots: discoveredRoots.map((root) => root.path),
        warnings: [...new Set(warnings)],
        conflicts: [...new Set(conflicts)],
        recovery_commands: [
            "soloforge doctor",
            "soloforge config repair --dry-run",
            "soloforge init --interactive",
            "soloforge sync-templates",
            "soloforge sync-adapters",
        ],
    };
}
export function resolveTaskScope(input) {
    if (input.routeDecision && !input.routeDecision.mutation_allowed) {
        const outputArtifact = input.routeDecision.output_artifact;
        const allowedPaths = [];
        if (outputArtifact?.path) {
            const dir = outputArtifact.path.replace(/\\/g, "/").replace(/\/[^/]+$/, "");
            allowedPaths.push(ensureTrailingSlash(dir || "."));
        }
        else {
            allowedPaths.push(".soloforge/output/");
        }
        const readonlyPaths = dedupePreserveOrder(input.config.repos.flatMap((repo) => (repo.scope ?? []).map((scope) => normalizeRelPath(scope, repo.path))));
        const candidates = allowedPaths.map((allowed) => ({
            path: allowed,
            source: "route_decision.output_artifact",
            confidence: "high",
            role: "output",
            evidence: ["read-only route may only write declared output artifact directory"],
        }));
        return {
            allowed_paths: allowedPaths,
            readonly_paths: readonlyPaths,
            new_files_allowed: true,
            new_file_patterns: ["*.md"],
            resolution: buildReport(input, allowedPaths, readonlyPaths, true, candidates, [], [], [], "resolved"),
        };
    }
    const warnings = [];
    const conflicts = [];
    const candidates = [];
    const discoveredRoots = discoverSourceRoots(input.projectPath);
    addRouteCandidates(input, candidates);
    addRepoCandidates(input, discoveredRoots, candidates, warnings);
    addConfigScopeCandidates(input, discoveredRoots, candidates, warnings);
    addFingerprintCandidates(input, discoveredRoots, candidates);
    const selected = removeNestedBroadDuplicates(candidates
        .filter((candidate) => candidate.confidence !== "low" || pathExists(input.projectPath, candidate.path))
        .map((candidate) => candidate.path));
    let allowedPaths = selected;
    let status = "resolved";
    if (allowedPaths.length === 0) {
        // 黑名单模式：空 scope 合法，不收窄，仅受内置安全黑名单约束
        status = "resolved";
    }
    if (isBroadRootOnly(allowedPaths) && discoveredRoots.length > 0) {
        allowedPaths = discoveredRoots.map((root) => root.path);
        status = "fallback";
        warnings.push("scope 仅为项目根，已收窄为真实源码根，避免过宽写入范围。");
    }
    const readonlyPaths = readonlyFromRepos(input)
        .filter((readonly) => !allowedPaths.some((allowed) => matchesScopePattern(readonly, allowed)
        || matchesScopePattern(allowed, readonly)
        || isPathInScope(readonly, allowed, { caseSensitive: PATH_CASE_SENSITIVE })
        || isPathInScope(allowed, readonly, { caseSensitive: PATH_CASE_SENSITIVE })));
    // 黑名单模式下始终允许新建文件（仅受内置黑名单约束）
    const newFilesAllowed = !input.degraded;
    const report = buildReport(input, allowedPaths, readonlyPaths, newFilesAllowed, candidates, discoveredRoots, warnings, conflicts, status);
    return {
        allowed_paths: allowedPaths,
        readonly_paths: readonlyPaths,
        new_files_allowed: newFilesAllowed,
        new_file_patterns: newFilesAllowed && input.classification.strategy === "full_pipeline"
            ? ["**/*"]
            : undefined,
        resolution: report,
    };
}
//# sourceMappingURL=scope_resolver.js.map