import os from 'node:os';
import path from 'node:path';
import { PACKAGE_VERSION, packageRoot, readJson, runProcess, which } from './fsx.js';
import { createRequestedScopeContract } from './safety/requested-scope-contract.js';
import { guardedPackageInstall, guardContextForRoute } from './safety/mutation-guard.js';
const DEFAULT_REGISTRY = 'https://registry.npmjs.org/';
export async function runSksUpdateCheck(options = {}) {
    const packageName = options.packageName || 'sneakoscope';
    const registry = options.registry || DEFAULT_REGISTRY;
    const env = options.env || process.env;
    const npmBin = options.npmBin === undefined ? await which('npm') : options.npmBin;
    const effectiveOptions = {
        packageName,
        currentVersion: options.currentVersion || PACKAGE_VERSION,
        npmBin,
        env
    };
    if (options.timeoutMs !== undefined)
        effectiveOptions.timeoutMs = options.timeoutMs;
    if (options.maxOutputBytes !== undefined)
        effectiveOptions.maxOutputBytes = options.maxOutputBytes;
    const override = env[versionOverrideEnvName(packageName)];
    const effectivePromise = detectEffectiveSksVersion(effectiveOptions);
    const latestPromise = !override && npmBin
        ? runProcess(npmBin, ['view', packageName, 'version', '--silent', '--registry', registry], {
            env,
            timeoutMs: options.timeoutMs ?? 5000,
            maxOutputBytes: options.maxOutputBytes ?? 4096
        }).catch((err) => ({
            code: 1,
            stdout: '',
            stderr: err instanceof Error ? err.message : String(err)
        }))
        : Promise.resolve(null);
    const effective = await effectivePromise;
    const current = effective.current;
    if (override)
        return buildResult({ packageName, current, effective, latest: override, registry, npmBin });
    if (!npmBin) {
        return buildResult({
            packageName,
            current,
            effective,
            latest: null,
            registry,
            npmBin: null,
            error: 'npm not found on PATH'
        });
    }
    const result = await latestPromise;
    if (!result) {
        return buildResult({
            packageName,
            current,
            effective,
            latest: null,
            registry,
            npmBin,
            error: 'npm view failed'
        });
    }
    if (result.code !== 0) {
        return buildResult({
            packageName,
            current,
            effective,
            latest: null,
            registry,
            npmBin,
            error: `${result.stderr || result.stdout || 'npm view failed'}`.trim()
        });
    }
    return buildResult({
        packageName,
        current,
        effective,
        latest: String(result.stdout || '').trim().split(/\s+/).pop() || null,
        registry,
        npmBin
    });
}
export async function runSksUpdateNow(options = {}) {
    const packageName = options.packageName || 'sneakoscope';
    const registry = options.registry || DEFAULT_REGISTRY;
    const env = options.env || process.env;
    const npmBin = options.npmBin === undefined ? await which('npm') : options.npmBin;
    const cwd = os.homedir();
    const check = await runSksUpdateCheck({
        ...options,
        packageName,
        registry,
        npmBin,
        env
    });
    const requestedVersion = parseVersionText(options.version || '') || null;
    const installVersion = requestedVersion || check.latest;
    const npmArgs = installVersion ? sksGlobalInstallArgs(packageName, installVersion, registry) : [];
    const command = npmBin && npmArgs.length ? [npmBin, ...npmArgs].join(' ') : null;
    const globalRoot = npmBin ? await detectNpmGlobalRoot(npmBin, env, options).catch(() => null) : null;
    if (!npmBin) {
        return buildUpdateNowResult({
            packageName,
            from: check.current,
            latest: check.latest,
            requestedVersion,
            installVersion,
            npmBin: null,
            npmArgs,
            command,
            cwd,
            registry,
            globalRoot,
            status: 'unavailable',
            ok: false,
            installCode: null,
            error: 'npm not found on PATH'
        });
    }
    if (!installVersion) {
        return buildUpdateNowResult({
            packageName,
            from: check.current,
            latest: check.latest,
            requestedVersion,
            installVersion,
            npmBin,
            npmArgs,
            command,
            cwd,
            registry,
            globalRoot,
            status: 'unavailable',
            ok: false,
            installCode: null,
            error: check.error || 'latest version unavailable'
        });
    }
    if (!requestedVersion && check.latest && !check.update_available) {
        return buildUpdateNowResult({
            packageName,
            from: check.current,
            latest: check.latest,
            requestedVersion,
            installVersion,
            npmBin,
            npmArgs,
            command,
            cwd,
            registry,
            globalRoot,
            status: 'current',
            ok: true,
            installCode: null,
            error: null
        });
    }
    if (options.dryRun) {
        return buildUpdateNowResult({
            packageName,
            from: check.current,
            latest: check.latest,
            requestedVersion,
            installVersion,
            npmBin,
            npmArgs,
            command,
            cwd,
            registry,
            globalRoot,
            status: 'dry_run',
            ok: true,
            installCode: null,
            error: null
        });
    }
    const mutationLedgerRoot = env.SKS_MUTATION_LEDGER_ROOT || packageRoot();
    const installContract = createRequestedScopeContract({
        route: 'update',
        userRequest: command || `npm global install ${packageName}`,
        projectRoot: mutationLedgerRoot,
        overrides: { package_install: true }
    });
    const install = await guardedPackageInstall(guardContextForRoute(mutationLedgerRoot, installContract, command || `npm global install ${packageName}`), `${packageName}@${installVersion}`, {
        confirmed: true,
        command: npmBin,
        args: npmArgs,
        cwd,
        env,
        timeoutMs: options.timeoutMs ?? 10 * 60 * 1000,
        maxOutputBytes: options.maxOutputBytes ?? 128 * 1024
    }).catch((err) => ({
        code: 1,
        stdout: '',
        stderr: err instanceof Error ? err.message : String(err)
    }));
    const ok = install.code === 0;
    return buildUpdateNowResult({
        packageName,
        from: check.current,
        latest: check.latest,
        requestedVersion,
        installVersion,
        npmBin,
        npmArgs,
        command,
        cwd,
        registry,
        globalRoot,
        status: ok ? 'updated' : 'failed',
        ok,
        installCode: install.code,
        error: ok ? null : `${install.stderr || install.stdout || 'npm global install failed'}`.trim()
    });
}
export function sksGlobalInstallArgs(packageName, version, registry = DEFAULT_REGISTRY) {
    return ['install', '--global', `${packageName}@${version}`, '--registry', registry];
}
export async function detectEffectiveSksVersion(options = {}) {
    const packageName = options.packageName || 'sneakoscope';
    const env = options.env || process.env;
    const npmBin = options.npmBin === undefined ? await which('npm') : options.npmBin;
    const candidates = [];
    const errors = [];
    const add = (version, source) => {
        const parsed = parseVersionText(version || '');
        if (parsed)
            candidates.push({ version: parsed, source });
    };
    add(options.currentVersion || PACKAGE_VERSION, 'runtime');
    add(env.SKS_INSTALLED_SKS_VERSION, 'env:SKS_INSTALLED_SKS_VERSION');
    const packageRootPromise = readJson(path.join(packageRoot(), 'package.json'), {}).catch(() => ({}));
    const pathSksPromise = which('sks')
        .then(async (sks) => {
        if (!sks)
            return null;
        const result = await runProcess(sks, ['--version'], {
            timeoutMs: 2000,
            maxOutputBytes: 4096,
            env: { ...env, SKS_DISABLE_UPDATE_CHECK: '1' }
        }).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
        return { sks, result };
    })
        .catch(() => null);
    const npmGlobalPromise = npmBin
        ? detectNpmGlobalPackageVersion(npmBin, packageName, env, {
            timeoutMs: options.timeoutMs ?? 2500,
            maxOutputBytes: options.maxOutputBytes ?? 8192
        }).catch((err) => ({ version: null, error: err?.message || String(err) }))
        : Promise.resolve(null);
    const [pkg, pathSks, npmGlobal] = await Promise.all([packageRootPromise, pathSksPromise, npmGlobalPromise]);
    add(pkg?.version, 'packageRoot:package.json');
    if (pathSks?.sks) {
        if (pathSks.result.code === 0)
            add(pathSks.result.stdout, `PATH:${pathSks.sks}`);
        else
            errors.push(`path_sks_version:${String(pathSks.result.stderr || pathSks.result.stdout || 'failed').trim()}`);
    }
    if (npmGlobal) {
        add(npmGlobal.version, `npm-global:${packageName}`);
        if (npmGlobal.error)
            errors.push(`npm_global_version:${npmGlobal.error}`);
    }
    const pathCandidate = candidates.find((candidate) => candidate.source.startsWith('PATH:'))?.version || null;
    const npmGlobalCandidate = candidates.find((candidate) => candidate.source.startsWith('npm-global:'))?.version || null;
    const packageRootCandidate = candidates.find((candidate) => candidate.source === 'packageRoot:package.json')?.version || null;
    const current = highestPackageVersion(candidates.map((candidate) => candidate.version));
    return {
        current,
        runtime_current: PACKAGE_VERSION,
        package_root_current: packageRootCandidate,
        path_current: pathCandidate,
        npm_global_current: npmGlobalCandidate,
        candidates,
        errors
    };
}
async function detectNpmGlobalPackageVersion(npmBin, packageName, env, opts) {
    const result = await runProcess(npmBin, ['list', '-g', packageName, '--json', '--depth=0', '--silent'], {
        env,
        timeoutMs: opts.timeoutMs,
        maxOutputBytes: opts.maxOutputBytes
    }).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
    if (result.code === 0 && result.stdout) {
        try {
            const parsed = JSON.parse(result.stdout);
            const version = parseVersionText(parsed?.dependencies?.[packageName]?.version || '');
            if (version)
                return { version };
        }
        catch { }
    }
    const rootResult = await runProcess(npmBin, ['root', '-g', '--silent'], {
        env,
        timeoutMs: opts.timeoutMs,
        maxOutputBytes: opts.maxOutputBytes
    }).catch((err) => ({ code: 1, stdout: '', stderr: err?.message || String(err) }));
    const root = String(rootResult.stdout || '').trim().split(/\r?\n/).pop();
    if (root) {
        const pkg = await readJson(path.join(root, packageName, 'package.json'), null).catch(() => null);
        const version = parseVersionText(pkg?.version || '');
        if (version)
            return { version };
    }
    return { version: null, error: String(result.stderr || result.stdout || rootResult.stderr || 'npm global package not found').trim() };
}
export function formatSksUpdateCheckText(result) {
    const lines = [
        'Update Check',
        `Current: ${result.current}`,
        `Latest:  ${result.latest || 'unknown'}`,
        `Update:  ${result.update_available ? 'available' : 'not needed'}`
    ];
    if (result.error)
        lines.push(`Error:   ${result.error}`);
    if (result.command)
        lines.push(`Run:     ${result.command}`);
    lines.push('Mode:    function-only');
    return lines.join('\n');
}
export function comparePackageVersions(a, b) {
    const pa = String(a || '').split(/[.-]/).map((x) => Number.parseInt(x, 10) || 0);
    const pb = String(b || '').split(/[.-]/).map((x) => Number.parseInt(x, 10) || 0);
    for (let i = 0; i < Math.max(pa.length, pb.length, 3); i += 1) {
        if ((pa[i] || 0) > (pb[i] || 0))
            return 1;
        if ((pa[i] || 0) < (pb[i] || 0))
            return -1;
    }
    return 0;
}
function buildResult(input) {
    const updateAvailable = Boolean(input.latest && comparePackageVersions(input.latest, input.current) > 0);
    return {
        schema: 'sks.update-check.v2',
        package: input.packageName,
        current: input.current,
        runtime_current: PACKAGE_VERSION,
        package_root_current: input.effective.package_root_current,
        path_current: input.effective.path_current,
        npm_global_current: input.effective.npm_global_current,
        version_candidates: input.effective.candidates,
        latest: input.latest,
        update_available: updateAvailable,
        status: input.error ? 'unavailable' : updateAvailable ? 'available' : 'current',
        mode: 'function',
        route_required: false,
        pipeline_required: false,
        command: updateAvailable ? `sks update now --version ${input.latest}` : null,
        npm_bin: input.npmBin,
        registry: input.registry,
        error: input.error || null
    };
}
function buildUpdateNowResult(input) {
    return {
        schema: 'sks.update-now.v1',
        ok: input.ok,
        status: input.status,
        package: input.packageName,
        from: input.from,
        latest: input.latest,
        requested_version: input.requestedVersion,
        install_version: input.installVersion,
        npm_bin: input.npmBin,
        npm_args: input.npmArgs,
        command: input.command,
        cwd: input.cwd,
        registry: input.registry,
        global_root: input.globalRoot,
        install_code: input.installCode,
        error: input.error
    };
}
async function detectNpmGlobalRoot(npmBin, env, opts = {}) {
    const result = await runProcess(npmBin, ['root', '--global', '--silent'], {
        env,
        timeoutMs: opts.timeoutMs ?? 2500,
        maxOutputBytes: opts.maxOutputBytes ?? 4096
    }).catch(() => ({ code: 1, stdout: '', stderr: '' }));
    return result.code === 0 ? String(result.stdout || '').trim().split(/\r?\n/).pop() || null : null;
}
function versionOverrideEnvName(packageName) {
    return `SKS_NPM_VIEW_${packageName.replace(/[^A-Za-z0-9]+/g, '_').toUpperCase()}_VERSION`;
}
function parseVersionText(text) {
    const match = String(text || '').match(/\b\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?\b/);
    return match ? match[0] : null;
}
function highestPackageVersion(versions) {
    return versions
        .filter((version) => typeof version === 'string' && version.length > 0)
        .reduce((best, candidate) => comparePackageVersions(candidate, best) > 0 ? candidate : best, '0.0.0');
}
//# sourceMappingURL=update-check.js.map