// System dependency checker for gjsify CLI.
// Uses execFileSync with explicit argument arrays — no shell injection possible.
// All binary names are hardcoded constants, never derived from user input.
//
// Severity model:
//   - 'required'  → must be present, exit code 1 if missing
//   - 'optional'  → nice to have, only warned about, exit code stays 0
//
// Conditional checking: when a project's package.json is reachable from cwd,
// optional system deps are only checked if the corresponding @gjsify/* package
// is in the project's dependency tree. A user with only @gjsify/fs in their
// project never sees a warning about libmanette.
import { execFileSync } from 'node:child_process';
import { join, resolve } from 'node:path';
import { createRequire } from 'node:module';
import { pathToFileURL } from 'node:url';
import { readFileSync, existsSync, readdirSync } from 'node:fs';
/** Run a binary and return its stdout trimmed, or null if it fails. */
function tryExecFile(binary, args) {
    try {
        return execFileSync(binary, args, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
    }
    catch {
        return null;
    }
}
/** Check if a binary exists and optionally capture its version output. */
function checkBinary(id, name, binary, versionArgs, severity, parseVersion, requiredBy) {
    const out = tryExecFile(binary, versionArgs);
    if (out === null)
        return { id, name, found: false, severity, requiredBy };
    const version = parseVersion ? parseVersion(out) : (out.split('\n')[0] ?? out);
    return { id, name, found: true, version, severity, requiredBy };
}
/** Check a pkg-config library. pkg-config --modversion returns version on stdout. */
function checkPkgConfig(id, name, libName, severity, requiredBy) {
    const version = tryExecFile('pkg-config', ['--modversion', libName]);
    if (version === null)
        return { id, name, found: false, severity, requiredBy };
    return { id, name, found: true, version: version.split('\n')[0], severity, requiredBy };
}
/**
 * Check for an npm package. Tries the user's project first (cwd), then falls
 * back to the CLI's own node_modules. This way a locally installed version
 * takes precedence, but npx usage still works via the CLI's own dependencies.
 */
function checkNpmPackage(id, name, packageName, cwd, severity, requiredBy) {
    // 1. Try user's project
    try {
        const requireFromCwd = createRequire(pathToFileURL(join(cwd, '_check_.js')).href);
        requireFromCwd.resolve(packageName);
        return { id, name, found: true, severity, requiredBy };
    }
    catch {
        /* not in project, try CLI fallback */
    }
    // 2. Fallback: CLI's own node_modules
    try {
        const requireFromCli = createRequire(import.meta.url);
        requireFromCli.resolve(packageName);
        return { id, name, found: true, severity, requiredBy };
    }
    catch {
        return { id, name, found: false, severity, requiredBy };
    }
}
export function detectPackageManager() {
    const managers = ['apt', 'dnf', 'pacman', 'zypper', 'apk'];
    for (const pm of managers) {
        const result = tryExecFile('which', [pm]);
        if (result !== null)
            return pm;
    }
    return 'unknown';
}
/** Optional system deps keyed by their DepCheck id. */
const OPTIONAL_DEPS = {
    manette: { id: 'manette', name: 'libmanette', pkgName: 'manette-0.2' },
    gstreamer: { id: 'gstreamer', name: 'GStreamer', pkgName: 'gstreamer-1.0' },
    'gst-app': { id: 'gst-app', name: 'GStreamer App', pkgName: 'gstreamer-app-1.0' },
    'gdk-pixbuf': { id: 'gdk-pixbuf', name: 'GdkPixbuf', pkgName: 'gdk-pixbuf-2.0' },
    pango: { id: 'pango', name: 'Pango', pkgName: 'pango' },
    pangocairo: { id: 'pangocairo', name: 'PangoCairo', pkgName: 'pangocairo' },
    webkitgtk: { id: 'webkitgtk', name: 'WebKitGTK', pkgName: 'webkitgtk-6.0' },
    cairo: { id: 'cairo', name: 'Cairo', pkgName: 'cairo' },
    // Build-time deps for @gjsify/*-native Vala prebuilds. End-users with
    // installed prebuilds don't need the -devel package — only contributors
    // rebuilding from source via `yarn build:prebuilds`. We still surface
    // them in the optional set so the "missing" diagnostic catches build-
    // time failures with an actionable install-hint instead of a meson
    // `Run-time dependency X found: NO` error mid-build.
    gnutls: { id: 'gnutls', name: 'GnuTLS', pkgName: 'gnutls' },
    nghttp2: { id: 'nghttp2', name: 'libnghttp2', pkgName: 'libnghttp2' },
};
/**
 * Map of @gjsify/* package name → ids of OPTIONAL_DEPS this package needs.
 * Generated by walking each package's `gi://` imports (excluding the always-
 * available trio GLib/GObject/Gio).
 *
 * Used to compute the set of optional deps to check for a given project.
 */
const PACKAGE_DEPS = {
    '@gjsify/gamepad': ['manette'],
    '@gjsify/webaudio': ['gstreamer', 'gst-app'],
    '@gjsify/iframe': ['webkitgtk'],
    '@gjsify/canvas2d': ['gdk-pixbuf', 'pango', 'pangocairo', 'cairo'],
    '@gjsify/canvas2d-core': ['gdk-pixbuf', 'pango', 'pangocairo', 'cairo'],
    '@gjsify/dom-elements': ['gdk-pixbuf'],
    // @gjsify/webgl needs the gwebgl npm package (Vala prebuild) — handled
    // as a special-case checkNpmPackage rather than checkPkgConfig in
    // runOptionalChecks. Mapping it here so its presence in the project's
    // dep tree triggers the check.
    '@gjsify/webgl': ['gwebgl'],
    // Native Vala bridges with `dependency('gnutls')` / `dependency('libnghttp2')`
    // in their meson.build. Optional because the shipped prebuild covers the
    // common-arch user path; only contributors rebuilding from source hit the
    // build-time dep.
    '@gjsify/tls-native': ['gnutls'],
    '@gjsify/http2-native': ['nghttp2'],
    // @gjsify/event-bridge only needs gtk4/gdk which are already in the
    // required set, so it doesn't need an optional entry.
};
/** Walk up from cwd looking for the nearest package.json. */
function findProjectRoot(cwd) {
    let dir = resolve(cwd);
    while (true) {
        if (existsSync(join(dir, 'package.json')))
            return dir;
        const parent = resolve(dir, '..');
        if (parent === dir)
            return null;
        dir = parent;
    }
}
/**
 * Discover which @gjsify/* packages a project depends on (transitively),
 * by walking the node_modules tree from the project root. Returns a Set of
 * package names like '@gjsify/webgl', '@gjsify/canvas2d'.
 *
 * If the project root cannot be determined or has no node_modules, returns
 * `null` to signal "check all optional deps" — this matches the historical
 * behaviour where `gjsify check` outside any project shows the full list.
 */
function discoverGjsifyPackages(cwd) {
    const root = findProjectRoot(cwd);
    if (!root)
        return null;
    const nodeModulesDir = join(root, 'node_modules', '@gjsify');
    if (!existsSync(nodeModulesDir)) {
        // Project exists but no @gjsify/* installed → only need core checks.
        return new Set();
    }
    // Read top-level package.json to determine if @gjsify/cli is the only
    // dep (in which case we still want to warn about everything the CLI
    // transitively brings in). Otherwise scan node_modules for installed packages.
    const pkgJsonPath = join(root, 'package.json');
    let topPkg = {};
    try {
        topPkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
    }
    catch {
        /* ignore */
    }
    const directDeps = {
        ...topPkg.dependencies,
        ...topPkg.devDependencies,
    };
    const found = new Set();
    try {
        for (const entry of readdirSync(nodeModulesDir, { withFileTypes: true })) {
            if (entry.isDirectory() || entry.isSymbolicLink()) {
                found.add(`@gjsify/${entry.name}`);
            }
        }
    }
    catch {
        /* ignore */
    }
    // Also include direct deps even if node_modules walk failed
    for (const dep of Object.keys(directDeps)) {
        if (dep.startsWith('@gjsify/'))
            found.add(dep);
    }
    return found;
}
/**
 * Compute the set of optional dep ids that should be checked for the
 * current project. Returns null to indicate "check all" (no project context).
 */
function computeNeededOptionalDeps(cwd) {
    const installedPackages = discoverGjsifyPackages(cwd);
    if (installedPackages === null)
        return null; // no project → check everything
    const needed = new Set();
    for (const pkg of installedPackages) {
        const deps = PACKAGE_DEPS[pkg];
        if (deps)
            for (const id of deps)
                needed.add(id);
    }
    return needed;
}
/**
 * Run all dependency checks. Used by `gjsify check` to show full system status.
 *
 * Required deps (gjs, gtk4, libsoup3, libadwaita, gobject-introspection,
 * blueprint-compiler, pkg-config, meson) are always checked.
 *
 * Optional deps are checked conditionally based on which @gjsify/* packages
 * the project (resolved from cwd) actually consumes. If no project context
 * is available, all optional deps are checked.
 */
export function runAllChecks(cwd) {
    const needed = computeNeededOptionalDeps(cwd);
    return [...runMinimalChecks(), ...runRequiredChecks(cwd), ...runOptionalChecks(needed, cwd)];
}
/**
 * Minimal checks needed to run any GJS example (Node + GJS binaries only).
 * Used by `gjsify showcase` for examples that have no native deps.
 */
export function runMinimalChecks() {
    const results = [];
    // Node.js — always present
    results.push({ id: 'nodejs', name: 'Node.js', found: true, version: process.version, severity: 'required' });
    // GJS
    results.push(checkBinary('gjs', 'GJS', 'gjs', ['--version'], 'required', (out) => out.replace(/^GJS\s+/i, '').split('\n')[0] ?? out));
    return results;
}
/** Check gwebgl npm package (project first, CLI fallback). Optional — only needed by @gjsify/webgl users. */
export function checkGwebgl(cwd) {
    return checkNpmPackage('gwebgl', 'gwebgl (@gjsify/webgl)', '@gjsify/webgl', cwd, 'optional', ['@gjsify/webgl']);
}
/**
 * Required system dependencies — always checked, missing → exit 1.
 * Includes the core build toolchain (pkg-config, meson, blueprint-compiler)
 * and the foundational libraries (gtk4, libadwaita, libsoup3,
 * gobject-introspection) that nearly every gjsify app needs.
 */
function runRequiredChecks(_cwd) {
    const results = [];
    // Build toolchain
    results.push(checkBinary('blueprint-compiler', 'Blueprint Compiler', 'blueprint-compiler', ['--version'], 'required'));
    results.push(checkBinary('pkg-config', 'pkg-config', 'pkg-config', ['--version'], 'required'));
    results.push(checkBinary('meson', 'Meson', 'meson', ['--version'], 'required'));
    // Foundational libraries
    results.push(checkPkgConfig('gtk4', 'GTK4', 'gtk4', 'required'));
    results.push(checkPkgConfig('libadwaita', 'libadwaita', 'libadwaita-1', 'required'));
    results.push(checkPkgConfig('libsoup3', 'libsoup3', 'libsoup-3.0', 'required'));
    results.push(checkPkgConfig('gobject-introspection', 'GObject Introspection', 'gobject-introspection-1.0', 'required'));
    return results;
}
/**
 * Optional system dependencies — only checked if the corresponding @gjsify/*
 * package is in use. Missing optional deps generate warnings, not errors.
 *
 * @param needed Set of optional dep ids to check, or null to check all.
 * @param cwd Used to resolve the gwebgl npm package check.
 */
function runOptionalChecks(needed, cwd) {
    const results = [];
    for (const [id, dep] of Object.entries(OPTIONAL_DEPS)) {
        if (needed !== null && !needed.has(id))
            continue;
        const requiredBy = Object.entries(PACKAGE_DEPS)
            .filter(([, ids]) => ids.includes(id))
            .map(([pkg]) => pkg);
        results.push(checkPkgConfig(dep.id, dep.name, dep.pkgName, 'optional', requiredBy));
    }
    // gwebgl npm package — special case (not a pkg-config lib). Only checked
    // when the project directly or transitively depends on @gjsify/webgl —
    // since the CLI tarball no longer ships any showcase example packages,
    // gwebgl is not part of the CLI's own dep tree, so reporting it for every
    // project would always be `found: false`. `needed === null` means "check
    // everything" (no project context).
    const hasWebglDep = needed === null || needed.has('gwebgl');
    if (hasWebglDep) {
        results.push(checkGwebgl(cwd));
    }
    return results;
}
// Per-package-manager install package names, keyed by dep id.
// Entries with multiple space-separated packages are intentional (one dep needs multiple system pkgs).
const PM_PACKAGES = {
    apt: {
        gjs: 'gjs',
        'blueprint-compiler': 'blueprint-compiler',
        'pkg-config': 'pkg-config',
        meson: 'meson',
        gtk4: 'libgtk-4-dev',
        libadwaita: 'libadwaita-1-dev',
        libsoup3: 'libsoup-3.0-dev',
        webkitgtk: 'libwebkit2gtk-6.0-dev',
        'gobject-introspection': 'gobject-introspection libgirepository1.0-dev',
        manette: 'libmanette-0.2-0 gir1.2-manette-0.2',
        gstreamer: 'libgstreamer1.0-dev',
        'gst-app': 'libgstreamer-plugins-base1.0-dev gir1.2-gst-plugins-base-1.0',
        'gdk-pixbuf': 'libgdk-pixbuf-2.0-dev',
        pango: 'libpango1.0-dev',
        pangocairo: 'libpango1.0-dev',
        cairo: 'libcairo2-dev',
        gnutls: 'libgnutls28-dev',
        nghttp2: 'libnghttp2-dev',
    },
    dnf: {
        gjs: 'gjs',
        'blueprint-compiler': 'blueprint-compiler',
        'pkg-config': 'pkgconf-pkg-config',
        meson: 'meson',
        gtk4: 'gtk4-devel',
        libadwaita: 'libadwaita-devel',
        libsoup3: 'libsoup3-devel',
        webkitgtk: 'webkitgtk6.0-devel',
        'gobject-introspection': 'gobject-introspection-devel',
        manette: 'libmanette-devel',
        gstreamer: 'gstreamer1-devel',
        'gst-app': 'gstreamer1-plugins-base-devel',
        'gdk-pixbuf': 'gdk-pixbuf2-devel',
        pango: 'pango-devel',
        pangocairo: 'pango-devel',
        cairo: 'cairo-devel',
        gnutls: 'gnutls-devel',
        nghttp2: 'libnghttp2-devel',
    },
    pacman: {
        gjs: 'gjs',
        'blueprint-compiler': 'blueprint-compiler',
        'pkg-config': 'pkgconf',
        meson: 'meson',
        gtk4: 'gtk4',
        libadwaita: 'libadwaita',
        libsoup3: 'libsoup3',
        webkitgtk: 'webkitgtk-6.0',
        'gobject-introspection': 'gobject-introspection',
        manette: 'libmanette',
        gstreamer: 'gstreamer',
        'gst-app': 'gst-plugins-base',
        'gdk-pixbuf': 'gdk-pixbuf2',
        pango: 'pango',
        pangocairo: 'pango',
        cairo: 'cairo',
        gnutls: 'gnutls',
        nghttp2: 'libnghttp2',
    },
    zypper: {
        gjs: 'gjs',
        'blueprint-compiler': 'blueprint-compiler',
        'pkg-config': 'pkg-config',
        meson: 'meson',
        gtk4: 'gtk4-devel',
        libadwaita: 'libadwaita-devel',
        libsoup3: 'libsoup-3_0-devel',
        webkitgtk: 'webkitgtk6_0-devel',
        'gobject-introspection': 'gobject-introspection-devel',
        manette: 'libmanette-0_2-0-devel',
        gstreamer: 'gstreamer-devel',
        'gst-app': 'gstreamer-plugins-base-devel',
        'gdk-pixbuf': 'gdk-pixbuf-devel',
        pango: 'pango-devel',
        pangocairo: 'pango-devel',
        cairo: 'cairo-devel',
        gnutls: 'libgnutls-devel',
        nghttp2: 'libnghttp2-devel',
    },
    apk: {
        gjs: 'gjs',
        'blueprint-compiler': 'blueprint-compiler',
        'pkg-config': 'pkgconf',
        meson: 'meson',
        gtk4: 'gtk4.0-dev',
        libadwaita: 'libadwaita-dev',
        libsoup3: 'libsoup3-dev',
        webkitgtk: 'webkit2gtk-6.0-dev',
        'gobject-introspection': 'gobject-introspection-dev',
        manette: 'libmanette-dev',
        gstreamer: 'gstreamer-dev',
        'gst-app': 'gst-plugins-base-dev',
        'gdk-pixbuf': 'gdk-pixbuf-dev',
        pango: 'pango-dev',
        pangocairo: 'pango-dev',
        cairo: 'cairo-dev',
        gnutls: 'gnutls-dev',
        nghttp2: 'nghttp2-dev',
    },
    unknown: {},
};
const PM_INSTALL_PREFIX = {
    apt: 'sudo apt install',
    dnf: 'sudo dnf install',
    pacman: 'sudo pacman -S',
    zypper: 'sudo zypper install',
    apk: 'sudo apk add',
    unknown: '',
};
/**
 * Build a suggested install command for missing dependencies.
 * gwebgl is an npm package, not a system package — handled separately.
 * Returns null when package manager is unknown or no installable deps are missing.
 */
export function buildInstallCommand(pm, missing) {
    if (pm === 'unknown')
        return null;
    const pkgMap = PM_PACKAGES[pm];
    const pkgs = [];
    const npmDeps = [];
    for (const dep of missing) {
        if (dep.id === 'gwebgl') {
            npmDeps.push('@gjsify/webgl');
            continue;
        }
        if (dep.id === 'nodejs')
            continue; // can't be missing if we're running
        const pkg = pkgMap[dep.id];
        if (pkg)
            pkgs.push(pkg);
    }
    const lines = [];
    if (pkgs.length > 0) {
        lines.push(`${PM_INSTALL_PREFIX[pm]} ${pkgs.join(' ')}`);
    }
    if (npmDeps.length > 0) {
        lines.push(`npm install ${npmDeps.join(' ')}`);
    }
    return lines.length > 0 ? lines.join('\n  ') : null;
}
