"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEFAULT_TRAVERSAL_BUDGETS = void 0;
exports.validateTraversalBudgets = validateTraversalBudgets;
exports.traverseWorkspace = traverseWorkspace;
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
const workspace_ignore_1 = require("../workspace-ignore");
exports.DEFAULT_TRAVERSAL_BUDGETS = Object.freeze({ files: 10_000, directories: 2_000, metadataBytes: 16_000_000, contentBytes: 4_000_000, elapsedMs: 5_000, evidenceEntries: 1_000 });
function validateTraversalBudgets(input) {
    for (const [name, value] of Object.entries(input))
        if (!Number.isSafeInteger(value) || value <= 0)
            throw new TypeError(`Traversal budget '${name}' must be a positive safe integer.`);
    return { ...input };
}
function traverseWorkspace(options) {
    const limits = validateTraversalBudgets({ ...exports.DEFAULT_TRAVERSAL_BUDGETS, ...options.budgets });
    const usage = { files: 0, directories: 0, metadataBytes: 0, contentBytes: 0, elapsedMs: 0, evidenceEntries: 0 };
    const entries = [];
    const findings = [];
    const root = node_path_1.default.resolve(options.workspaceRoot);
    const now = options.now ?? Date.now;
    const started = now();
    const startPath = options.startPath?.replace(/\\/g, "/").replace(/^\.\//, "") ?? "";
    const startAbsolute = node_path_1.default.resolve(root, startPath);
    const startBoundary = node_path_1.default.relative(root, startAbsolute);
    if (startBoundary === ".." || startBoundary.startsWith(`..${node_path_1.default.sep}`) || node_path_1.default.isAbsolute(startBoundary))
        throw new TypeError("Traversal start path must remain inside the workspace root.");
    const pending = [startPath];
    const finish = (budget, nextPath) => {
        const omittedPath = nextPath || ".";
        const continuationPath = node_path_1.default.posix.dirname(omittedPath.replace(/\\/g, "/")) || ".";
        return { complete: false, entries, findings, usage: { ...usage, elapsedMs: Math.max(0, now() - started) }, limits, exhaustedBudget: budget, omission: { nextPath: omittedPath, reason: `Stopped before exceeding ${budget}.` }, recovery: { command: `day-shift doctor --scan-surface --scan-path ${JSON.stringify(continuationPath)} --format json`, guidance: "Continue from the omitted path's containing subtree; use --repository-audit only when a larger bounded result is explicitly needed." }, healthClaim: "not-assessed" };
    };
    while (pending.length) {
        const relative = pending.shift();
        usage.elapsedMs = Math.max(0, now() - started);
        if (usage.elapsedMs >= limits.elapsedMs)
            return finish("elapsedMs", relative);
        const absolute = node_path_1.default.resolve(root, relative);
        const boundary = node_path_1.default.relative(root, absolute);
        if (boundary === ".." || boundary.startsWith(`..${node_path_1.default.sep}`) || node_path_1.default.isAbsolute(boundary)) {
            findings.push({ code: "ENTRY_INACCESSIBLE", path: relative, message: "Entry resolved outside workspace containment." });
            continue;
        }
        if (relative && (0, workspace_ignore_1.explainIgnorePolicy)(options.policy, relative).decision === "exclude")
            continue;
        let stat;
        try {
            options.beforeLstat?.(absolute);
            stat = node_fs_1.default.lstatSync(absolute);
        }
        catch (error) {
            if (findings.length < limits.evidenceEntries)
                findings.push({ code: "ENTRY_INACCESSIBLE", path: relative || ".", message: error instanceof Error ? error.message : String(error) });
            continue;
        }
        const kind = stat.isSymbolicLink() ? "symlink" : stat.isDirectory() ? "directory" : "file";
        const metadataBytes = Buffer.byteLength(JSON.stringify({ path: relative, kind, size: stat.size, mtimeMs: stat.mtimeMs }));
        if (usage.metadataBytes + metadataBytes > limits.metadataBytes)
            return finish("metadataBytes", relative);
        if (usage.evidenceEntries + 1 > limits.evidenceEntries)
            return finish("evidenceEntries", relative);
        if (kind === "directory" && usage.directories + 1 > limits.directories)
            return finish("directories", relative);
        if (kind === "file" && usage.files + 1 > limits.files)
            return finish("files", relative);
        let contentBytesRead = 0;
        if (kind === "file" && options.readContent) {
            if (usage.contentBytes + stat.size > limits.contentBytes)
                return finish("contentBytes", relative);
            try {
                options.beforeContentRead?.(absolute);
                contentBytesRead = node_fs_1.default.readFileSync(absolute).byteLength;
            }
            catch (error) {
                if (findings.length < limits.evidenceEntries)
                    findings.push({ code: "ENTRY_CHANGED", path: relative, message: error instanceof Error ? error.message : String(error) });
            }
            if (usage.contentBytes + contentBytesRead > limits.contentBytes)
                return finish("contentBytes", relative);
        }
        usage.metadataBytes += metadataBytes;
        usage.evidenceEntries += 1;
        usage.contentBytes += contentBytesRead;
        if (kind === "directory")
            usage.directories += 1;
        else if (kind === "file")
            usage.files += 1;
        entries.push({ path: relative || ".", kind, size: stat.size, contentBytesRead });
        if (kind === "symlink") {
            if (findings.length < limits.evidenceEntries)
                findings.push({ code: "SYMLINK_SKIPPED", path: relative, message: "Symbolic links are recorded but never followed." });
            continue;
        }
        if (kind === "directory") {
            let children;
            try {
                children = node_fs_1.default.readdirSync(absolute).sort((a, b) => a.localeCompare(b, "en"));
            }
            catch (error) {
                if (findings.length < limits.evidenceEntries)
                    findings.push({ code: "ENTRY_INACCESSIBLE", path: relative || ".", message: error instanceof Error ? error.message : String(error) });
                continue;
            }
            pending.unshift(...children.map((child) => relative ? `${relative}/${child}` : child));
        }
    }
    usage.elapsedMs = Math.max(0, now() - started);
    return { complete: true, entries, findings, usage, limits, healthClaim: "not-assessed" };
}
