/**
 * olam update — Friendly wrapper around `npm install -g @pleri/olam-cli@<channel>`.
 *
 * F1: core install flow with snapshot-prev → install → restore-on-failure (D24)
 *     + scan-and-reconcile skill symlinks (T22/OQ29).
 * F2: --to=<ver>, --rollback, --check, --channel=<ch>.
 * F3: --freeze, --force, upgrade_blocked field check.
 *
 * T23 note: self-replace while running is harmless on POSIX — the OS keeps the
 * old inode open until exit; the new binary is a separate inode. No special
 * handling needed.
 */
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { execSync } from 'node:child_process';
import pc from 'picocolors';
import { reconcileSkillSymlinks } from '../lib/symlink-reconcile.js';
const PACKAGE_NAME = '@pleri/olam-cli';
const CACHE_DIR = path.join(os.homedir(), '.olam', 'cache');
const LOG_DIR = path.join(os.homedir(), '.olam', 'log');
const LAST_STABLE_FILE = path.join(CACHE_DIR, 'last-stable.txt');
function defaultExec(cmd) {
    try {
        const stdout = execSync(cmd, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
        return { exitCode: 0, stdout, stderr: '' };
    }
    catch (err) {
        const e = err;
        return { exitCode: e.status ?? 1, stdout: e.stdout ?? '', stderr: e.stderr ?? '' };
    }
}
export function getCurrentVersion(_exec = defaultExec) {
    const result = _exec(`npm ls -g ${PACKAGE_NAME} --json --depth=0`);
    if (result.exitCode !== 0)
        return null;
    try {
        const parsed = JSON.parse(result.stdout);
        return parsed.dependencies?.[PACKAGE_NAME]?.version ?? null;
    }
    catch {
        return null;
    }
}
export function readLastStable(file = LAST_STABLE_FILE) {
    try {
        const v = fs.readFileSync(file, 'utf-8').trim();
        return v || null;
    }
    catch {
        return null;
    }
}
export function writeLastStable(version, file = LAST_STABLE_FILE) {
    fs.mkdirSync(path.dirname(file), { recursive: true });
    fs.writeFileSync(file, version, { mode: 0o644 });
}
function logUpdateFailure(stderr) {
    const ts = new Date().toISOString().slice(0, 10);
    const logFile = path.join(LOG_DIR, `update-${ts}.log`);
    try {
        fs.mkdirSync(LOG_DIR, { recursive: true });
        fs.appendFileSync(logFile, `[update-failure ${new Date().toISOString()}]\n${stderr}\n`, 'utf-8');
    }
    catch { /* best-effort */ }
}
/** Allowlist for channel/version values before shell interpolation (HIGH fix). */
const SAFE_VERSION_RE = /^[a-zA-Z0-9._-]{1,64}$/;
/**
 * Check whether the published package has upgrade_blocked: true in its package.json.
 * Returns true when the field is present and truthy; false otherwise (including on error).
 */
export function isUpgradeBlocked(channelOrVersion, _exec = defaultExec) {
    if (!SAFE_VERSION_RE.test(channelOrVersion))
        return false;
    const result = _exec(`npm view ${PACKAGE_NAME}@${channelOrVersion} olam.upgrade_blocked --json`);
    if (result.exitCode !== 0)
        return false;
    try {
        const val = JSON.parse(result.stdout.trim());
        return val === true;
    }
    catch {
        return false;
    }
}
export async function doUpdate(opts, _exec = defaultExec, _reconcile = reconcileSkillSymlinks, _readConfig, _getNpmRoot = defaultExec) {
    const readConfig = _readConfig ?? (async () => {
        const { readMachineConfig } = await import('@olam/core/src/config/machine-schema.js');
        return readMachineConfig();
    });
    const config = await readConfig();
    const channel = opts.channel ?? config?.channel ?? 'stable';
    const quiet = opts.quiet ?? false;
    const force = opts.force ?? false;
    // Allowlist channel/version before any shell interpolation (HIGH security fix)
    if (!SAFE_VERSION_RE.test(channel)) {
        if (!quiet)
            console.error(`Invalid channel/version: ${channel}`);
        return { action: 'failed', exitCode: 11 };
    }
    // F3: respect auto_update: false (frozen) unless --force
    if (!force && config && config.auto_update === false) {
        if (!quiet) {
            console.log('Updates frozen. Run `olam update --force` to override or `olam update --freeze` to manage.');
        }
        return { action: 'failed', exitCode: 11 };
    }
    // F3: check upgrade_blocked on target version (best-effort; don't block on error)
    if (!force) {
        const blocked = isUpgradeBlocked(channel, _exec);
        if (blocked) {
            if (!quiet) {
                console.error(`Version @${channel} has upgrade_blocked: true. Run with --force to override.`);
            }
            return { action: 'failed', exitCode: 11 };
        }
    }
    // Snapshot current version before any mutation (D24)
    const prevVersion = getCurrentVersion(_exec);
    if (prevVersion) {
        writeLastStable(prevVersion);
    }
    if (!quiet) {
        console.log(pc.cyan(`Installing ${PACKAGE_NAME}@${channel}…`));
    }
    const installResult = _exec(`npm install -g ${PACKAGE_NAME}@${channel}`);
    if (installResult.exitCode !== 0) {
        logUpdateFailure(installResult.stderr);
        if (!quiet) {
            console.error(pc.red('Update failed.'));
        }
        // Auto-restore (D24)
        const prev = readLastStable();
        if (prev) {
            if (!quiet) {
                console.log(pc.yellow(`Restoring to ${prev}…`));
            }
            const restoreResult = _exec(`npm install -g ${PACKAGE_NAME}@${prev}`);
            if (restoreResult.exitCode !== 0) {
                if (!quiet) {
                    console.error(pc.red(`Restore also failed. Run: npm install -g ${PACKAGE_NAME}@${prev} manually.`));
                }
            }
            else if (!quiet) {
                console.log(pc.yellow(`Restored to ${prev}.`));
            }
        }
        else if (!quiet) {
            console.error(pc.red('No previous version cached; cannot auto-restore.'));
        }
        return { action: 'restored', prevVersion: prev ?? undefined, exitCode: 11 };
    }
    // Success path: get new version + reconcile symlinks
    const newVersion = getCurrentVersion(_exec) ?? undefined;
    if (!quiet && newVersion) {
        console.log(pc.green(`Updated to ${newVersion}.`));
    }
    // Scan-and-reconcile skill symlinks (T22/OQ29)
    const npmRootResult = _getNpmRoot('npm root -g');
    let symlinkResult = { added: [], removed: [] };
    if (npmRootResult.exitCode === 0) {
        const npmRoot = npmRootResult.stdout.trim();
        const npmSkillsDir = path.join(npmRoot, '@pleri', 'olam-cli', 'plugin', 'skills');
        const claudeSkillsDir = path.join(os.homedir(), '.claude', 'skills');
        const rec = _reconcile(npmSkillsDir, claudeSkillsDir);
        symlinkResult = { added: rec.added, removed: rec.removed };
        if (!quiet && (rec.added.length > 0 || rec.removed.length > 0)) {
            if (rec.added.length > 0) {
                console.log(pc.dim(`Skills added: ${rec.added.join(', ')}`));
            }
            if (rec.removed.length > 0) {
                console.log(pc.dim(`Skills removed (dangling): ${rec.removed.join(', ')}`));
            }
        }
    }
    return {
        action: 'installed',
        version: newVersion,
        prevVersion: prevVersion ?? undefined,
        exitCode: 0,
        symlinks: symlinkResult,
    };
}
export async function doCheck(_exec = defaultExec) {
    const current = getCurrentVersion(_exec);
    const viewResult = _exec(`npm view ${PACKAGE_NAME} dist-tags.stable`);
    const latest = viewResult.exitCode === 0 ? viewResult.stdout.trim() || null : null;
    if (current && latest && current === latest) {
        return { action: 'already-current', current, latest, exitCode: 10 };
    }
    return { action: 'update-available', current, latest, exitCode: 0 };
}
export async function doRollback(_exec = defaultExec, _reconcile = reconcileSkillSymlinks, _getNpmRoot = defaultExec) {
    const prev = readLastStable();
    if (!prev) {
        return { action: 'nothing-to-rollback', exitCode: 14 };
    }
    // OQ26: if last-stable.txt already equals the current version → already rolled back
    const current = getCurrentVersion(_exec);
    if (current && current === prev) {
        return { action: 'already-at-cached', restoredVersion: prev, exitCode: 0 };
    }
    const result = _exec(`npm install -g ${PACKAGE_NAME}@${prev}`);
    if (result.exitCode !== 0) {
        return { action: 'failed', restoredVersion: prev, exitCode: 11 };
    }
    // Reconcile symlinks after restore
    const npmRootResult = _getNpmRoot('npm root -g');
    if (npmRootResult.exitCode === 0) {
        const npmRoot = npmRootResult.stdout.trim();
        _reconcile(path.join(npmRoot, '@pleri', 'olam-cli', 'plugin', 'skills'), path.join(os.homedir(), '.claude', 'skills'));
    }
    return { action: 'rolled-back', restoredVersion: prev, exitCode: 0 };
}
export function registerUpdate(program) {
    program
        .command('update')
        .description('Update @pleri/olam-cli to the latest version on the configured channel')
        .option('--to <version>', 'Install a specific version')
        .option('--rollback', 'Restore the previously installed version')
        .option('--check', 'Check if a newer version is available (read-only)')
        .option('--channel <channel>', 'Switch to channel and install (stable|beta|edge)')
        .option('--freeze', 'Disable automatic updates (sets auto_update: false)')
        .option('--force', 'Override freeze and upgrade_blocked checks')
        .option('--quiet', 'Suppress output')
        .action(async (opts) => {
        if (opts.freeze) {
            const { readMachineConfig, writeMachineConfig, MachineConfigSchema } = await import('@olam/core/src/config/machine-schema.js');
            const cfg = readMachineConfig() ?? MachineConfigSchema.parse({});
            writeMachineConfig({ ...cfg, auto_update: false });
            if (!opts.quiet)
                console.log('Updates frozen. Run `olam update --force` to override.');
            return;
        }
        if (opts.check) {
            const r = await doCheck();
            if (!opts.quiet) {
                if (r.action === 'already-current') {
                    console.log(`Already on latest: ${r.current}`);
                }
                else {
                    console.log(`Current: ${r.current ?? 'unknown'}, Latest: ${r.latest ?? 'unknown'}`);
                }
            }
            process.exitCode = r.exitCode;
            return;
        }
        if (opts.rollback) {
            const r = await doRollback();
            if (!opts.quiet) {
                if (r.action === 'nothing-to-rollback') {
                    console.error('Nothing to roll back. Use --to=<version> for a specific target.');
                }
                else if (r.action === 'already-at-cached') {
                    console.log(`Already at cached version ${r.restoredVersion}. Use --to=<v> for older targets.`);
                }
                else if (r.action === 'rolled-back') {
                    console.log(`Rolled back to ${r.restoredVersion}.`);
                }
                else {
                    console.error(`Rollback failed.`);
                }
            }
            process.exitCode = r.exitCode;
            return;
        }
        if (opts.channel) {
            // Write config then install
            const { readMachineConfig, writeMachineConfig, MachineConfigSchema } = await import('@olam/core/src/config/machine-schema.js');
            const cfg = readMachineConfig() ?? MachineConfigSchema.parse({});
            writeMachineConfig({ ...cfg, channel: opts.channel });
        }
        const result = await doUpdate({ channel: opts.to ?? opts.channel, quiet: opts.quiet, force: opts.force });
        process.exitCode = result.exitCode;
    });
}
//# sourceMappingURL=update.js.map