"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' };
}
