/**
 * @fileoverview Probe engine for `codeman doctor`. Resolves each registry tool
 * against an injectable ProbeHost (real impl uses child_process/fs; tests inject
 * fakes) and returns structured results. Pure given the host — no global I/O.
 *
 * @module utils/dependency-checker
 */
import { execFileSync } from 'node:child_process';
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import { EXEC_TIMEOUT_MS } from '../config/exec-timeout.js';
export function detectEnvironment(inputs) {
    if (inputs.platform === 'win32')
        return 'win32';
    if (inputs.platform === 'darwin')
        return 'darwin';
    const isWsl = /microsoft|wsl/i.test(inputs.procVersion) && inputs.hasWindowsInterop;
    return isWsl ? 'wsl' : 'linux';
}
const DEFAULT_VERSION_RE = /(\d+\.\d+(?:\.\d+)?)/;
export function extractVersion(text, re) {
    const m = (re ?? DEFAULT_VERSION_RE).exec(text);
    return m ? m[1] : undefined;
}
/** Returns -1 if a < b, 0 if equal, 1 if a > b (numeric, component-wise). */
export function compareVersions(a, b) {
    const pa = a.split('.').map((n) => parseInt(n, 10) || 0);
    const pb = b.split('.').map((n) => parseInt(n, 10) || 0);
    const len = Math.max(pa.length, pb.length);
    for (let i = 0; i < len; i++) {
        const d = (pa[i] || 0) - (pb[i] || 0);
        if (d !== 0)
            return d < 0 ? -1 : 1;
    }
    return 0;
}
function finalize(base, tool, path, version) {
    if (tool.minVersion) {
        if (!version)
            return { ...base, status: 'error', path, reason: 'version required but could not be parsed' };
        if (compareVersions(version, tool.minVersion) < 0)
            return { ...base, status: 'outdated', path, version };
    }
    return { ...base, status: 'ok', path, version };
}
export function checkTool(tool, host) {
    const base = {
        id: tool.id,
        label: tool.label,
        category: tool.category,
        required: tool.required,
        usedBy: tool.usedBy ?? [],
    };
    const installHint = tool.installHint?.[host.environment];
    const spec = tool.resolvers.find((r) => r.match.includes(host.environment));
    if (!spec)
        return { ...base, status: 'skipped', reason: `not applicable on ${host.environment}` };
    if (spec.resolver.kind === 'path') {
        const { bins, versionArg, versionRegex } = spec.resolver;
        for (const bin of bins) {
            const resolved = host.which(bin);
            if (resolved) {
                const out = host.runVersion(bin, [versionArg ?? '--version']);
                const version = out ? extractVersion(out, versionRegex) : undefined;
                return finalize(base, tool, resolved, version);
            }
        }
        return { ...base, status: 'missing', installHint };
    }
    // windows-side
    const { appDirs, exes } = spec.resolver;
    for (const root of host.windowsProgramRoots()) {
        for (const dir of appDirs) {
            for (const exe of exes) {
                const winPath = `${root}/${dir}/${exe}`;
                if (host.fileExists(winPath)) {
                    const raw = host.windowsFileVersion(winPath);
                    const version = raw ? extractVersion(raw) : undefined;
                    return finalize(base, tool, winPath, version);
                }
            }
        }
    }
    return { ...base, status: 'missing', installHint };
}
export function checkAll(registry, host) {
    return registry.map((tool) => checkTool(tool, host));
}
function safeWhich(bin) {
    try {
        const out = execFileSync(process.platform === 'win32' ? 'where' : 'which', [bin], {
            encoding: 'utf-8',
            timeout: EXEC_TIMEOUT_MS,
        }).trim();
        const first = out.split(/\r?\n/)[0]?.trim();
        return first && existsSync(first) ? first : null;
    }
    catch {
        return null;
    }
}
function safeRunVersion(bin, args) {
    try {
        return execFileSync(bin, args, {
            encoding: 'utf-8',
            timeout: EXEC_TIMEOUT_MS,
            stdio: ['ignore', 'pipe', 'ignore'],
        });
    }
    catch (err) {
        // Some tools (e.g. ffmpeg) exit non-zero on -version but still print to stdout
        const stdout = err?.stdout;
        return stdout ? stdout.toString() : null;
    }
}
function readProcVersion() {
    try {
        return readFileSync('/proc/version', 'utf-8');
    }
    catch {
        return '';
    }
}
function listWindowsProgramRoots() {
    const roots = [];
    try {
        for (const entry of readdirSync('/mnt')) {
            for (const pf of ['Program Files', 'Program Files (x86)']) {
                const root = `/mnt/${entry}/${pf}`;
                if (existsSync(root))
                    roots.push(root);
            }
        }
    }
    catch {
        // /mnt absent (not WSL) -> no roots
    }
    return roots;
}
function readWindowsFileVersion(winPath) {
    try {
        const windowsPath = execFileSync('wslpath', ['-w', winPath], {
            encoding: 'utf-8',
            timeout: EXEC_TIMEOUT_MS,
        }).trim();
        const out = execFileSync('powershell.exe', ['-NoProfile', '-Command', `(Get-Item '${windowsPath.replace(/'/g, "''")}').VersionInfo.ProductVersion`], { encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }).trim();
        return out || null;
    }
    catch {
        return null;
    }
}
export function createRealHost() {
    const environment = detectEnvironment({
        platform: process.platform,
        procVersion: readProcVersion(),
        hasWindowsInterop: safeWhich('cmd.exe') !== null || safeWhich('powershell.exe') !== null,
    });
    return {
        environment,
        which: safeWhich,
        fileExists: existsSync,
        runVersion: safeRunVersion,
        windowsProgramRoots: listWindowsProgramRoots,
        windowsFileVersion: readWindowsFileVersion,
    };
}
//# sourceMappingURL=dependency-checker.js.map