import * as fs from 'node:fs';
import * as path from 'node:path';
import { globSync } from 'glob';
import { DEFAULT_IGNORE_PATTERNS } from '../config-schema.js';
import { describeSafeExecError, safeExec } from '../sys/exec.js';
import { sanitizeForTerminal } from '../terminal-sanitize.js';
/**
 * Parse null-delimited git output into normalized forward-slash paths.
 */
function parseGitPaths(output) {
    return output
        .split('\0')
        .filter(Boolean)
        .map((f) => f.replace(/\\/g, '/'));
}
/**
 * Get the set of non-gitignored files in the project.
 * Includes files inside git submodules via --recurse-submodules.
 * Returns null if git is unavailable or the project is not a git repo.
 */
function getGitNonIgnoredFiles(projectRoot, onWarn) {
    const execOpts = { cwd: projectRoot };
    try {
        // Parent repo: tracked + untracked (non-ignored) files
        const output = safeExec('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard'], execOpts);
        const files = new Set(parseGitPaths(output));
        // Submodules: --recurse-submodules only supports --cached,
        // but submodule files we care about are always committed.
        try {
            const subOutput = safeExec('git', ['ls-files', '-z', '--cached', '--recurse-submodules'], execOpts);
            for (const f of parseGitPaths(subOutput)) {
                files.add(f);
            }
        }
        catch {
            // --recurse-submodules unsupported or no submodules — ignore
        }
        return files;
    }
    catch (err) {
        const errorMsg = describeSafeExecError(err);
        const msg = errorMsg.includes('ENOENT') || errorMsg.includes('not found')
            ? `Command 'git' not found. Cannot use .gitignore for filtering. Falling back to ignorePatterns only.`
            : `Could not read git index for .gitignore filtering. Falling back to ignorePatterns only. Error: ${errorMsg}`;
        if (onWarn) {
            onWarn(msg);
        }
        return null;
    }
}
/** Resolve glob patterns from config targets to actual file paths. */
export function resolveFiles(targets, projectRoot, ignorePatterns = DEFAULT_IGNORE_PATTERNS, onWarn) {
    const results = [];
    const seen = new Set();
    const nonIgnored = getGitNonIgnoredFiles(projectRoot, onWarn);
    for (const target of targets) {
        const matches = globSync(target.glob, {
            cwd: projectRoot,
            nodir: true,
            ignore: ignorePatterns,
        });
        for (const rawPath of matches) {
            const relativePath = rawPath.replace(/\\/g, '/');
            if (seen.has(relativePath))
                continue;
            if (nonIgnored && !nonIgnored.has(relativePath))
                continue;
            seen.add(relativePath);
            const absolutePath = path.join(projectRoot, rawPath);
            // Symlink guard (mmnto-ai/totem#2354): the downstream read follows the
            // link (fs.readFileSync), so a symlink under an ingest glob would pull its
            // TARGET content into the index — a link escaping the repo
            // (e.g. -> /etc/passwd, -> ~/.ssh/id_rsa) exfiltrates host files into a
            // queryable store, and the git-tracked-set gate does NOT catch it
            // (a symlink is a valid git entry, mode 120000). Skip symlinks entirely,
            // mirroring run-compiled-rules.ts's mode-120000 exclusion, and surface
            // each skip loudly so the drop is never silent (Tenet 13 sensor).
            let isSymlink = false;
            try {
                isSymlink = fs.lstatSync(absolutePath).isSymbolicLink();
                // totem-context: intentional fall-through — a raced/ENOENT lstat means there is no stat-able target at this path; treat it as a non-symlink and let the downstream read (which has its own error guard) degrade to honest-absent. Aborting sync over one unstattable path would be inconsistent with that skip-don't-abort read behavior.
            }
            catch {
                // degrade to non-symlink — see totem-context above
            }
            if (isSymlink) {
                if (onWarn) {
                    onWarn(`Skipping symlink under ingest target (not indexed): ${sanitizeForTerminal(relativePath)}`);
                }
                continue;
            }
            results.push({
                absolutePath,
                relativePath,
                target,
            });
        }
    }
    return results;
}
/** Validate that a git ref is safe (hex SHA, HEAD~N, branch name — no shell metacharacters). */
const SAFE_GIT_REF = /^[a-zA-Z0-9_./:~^{}\-]+$/;
/**
 * Get files changed since a given git ref (e.g., HEAD~1 or a commit SHA).
 * Also includes untracked files so new files are picked up on incremental sync.
 * Uses -z for null-delimited output consistent with getGitNonIgnoredFiles.
 */
export function getChangedFiles(projectRoot, sinceRef = 'HEAD~1', onWarn) {
    if (!SAFE_GIT_REF.test(sinceRef)) {
        if (onWarn) {
            onWarn(`Invalid git ref "${sinceRef}" — falling back to full sync.`);
        }
        return null;
    }
    try {
        const diffOutput = safeExec('git', ['diff', '-z', '--name-only', sinceRef], {
            cwd: projectRoot,
        });
        // Also pick up untracked files (new files not yet committed)
        let untrackedOutput = '';
        try {
            untrackedOutput = safeExec('git', ['ls-files', '-z', '--others', '--exclude-standard'], {
                cwd: projectRoot,
            });
        }
        catch (err) {
            if (onWarn) {
                onWarn(`Failed to list untracked files: ${describeSafeExecError(err)}`);
            }
        }
        const paths = new Set((diffOutput + untrackedOutput)
            .split('\0')
            .map((p) => p.replace(/\\/g, '/'))
            .filter(Boolean));
        return [...paths];
    }
    catch (err) {
        const msg = `Failed to get changed files from git. Error: ${describeSafeExecError(err)}`;
        if (onWarn) {
            onWarn(msg);
        }
        return null;
    }
}
/**
 * Get the current HEAD SHA for sync state tracking.
 */
export function getHeadSha(projectRoot, onWarn) {
    try {
        return safeExec('git', ['rev-parse', 'HEAD'], {
            cwd: projectRoot,
        });
    }
    catch (err) {
        if (onWarn) {
            onWarn(`Failed to read HEAD SHA: ${describeSafeExecError(err)}`);
        }
        return null;
    }
}
//# sourceMappingURL=file-resolver.js.map