/**
 * olam skills — sync, dry-run/diff, list-installed, show.
 *
 *   olam skills sync [--dry-run] [--atlas-user <u>]
 *   olam skills diff [--atlas-user <u>]            (alias of `sync --dry-run`)
 *   olam skills list                                (lists deployed artifacts)
 *   olam skills show <name>                         (shows details for a deployed artifact)
 */
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import * as readline from 'node:readline';
import pc from 'picocolors';
import { syncSkills, claudeDir, listSkillSources, skillSourceClonePath, } from '@olam/core/src/skill-sources/index.js';
import { printError, printSuccess, printHeader } from '../output.js';
import { applyOverlays, formatSummary } from '../lib/skills-apply-overlays.js';
function asMessage(err) {
    return err instanceof Error ? err.message : String(err);
}
// ---------------------------------------------------------------------------
// C1 — First-sync atlas-user picker
// ---------------------------------------------------------------------------
const ATLAS_USER_PICKER_RE = /^[a-z0-9][a-z0-9_-]{0,38}$/;
/** Read member-name directories from <clonePath>/members/ sorted alphabetically. */
function listMemberNames(clonePath) {
    const membersDir = path.join(clonePath, 'members');
    if (!fs.existsSync(membersDir))
        return [];
    try {
        return fs.readdirSync(membersDir)
            .filter((e) => {
            try {
                return fs.statSync(path.join(membersDir, e)).isDirectory();
            }
            catch {
                return false;
            }
        })
            .sort();
    }
    catch {
        return [];
    }
}
function formatSourcePrefix(cfg) {
    if (cfg === undefined)
        return '(none)';
    const p = cfg.prefix !== undefined ? `'${cfg.prefix}:'` : '(no prefix)';
    const s = cfg.prefixScope !== undefined && cfg.prefixScope.length > 0
        ? ` (scope: ${cfg.prefixScope.join(',')})`
        : '';
    return `${p}${s}`;
}
function defaultSourcePrefixSyncPrompt(input) {
    return new Promise((resolve) => {
        const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
        const newLabel = formatSourcePrefix(input.newConfig);
        let line;
        if (input.kind === 'first-time') {
            line =
                `\n${pc.cyan('Source-config:')} ${input.sourceName} suggests prefix ${pc.bold(newLabel)}.\n` +
                    `Adopt for this host? ${pc.dim('[Y/n] ')}`;
        }
        else {
            const oldLabel = formatSourcePrefix(input.priorConfig);
            line =
                `\n${pc.cyan('Source-config CHANGED:')} ${input.sourceName} changed from ${pc.bold(oldLabel)} to ${pc.bold(newLabel)}.\n` +
                    `Accept change? ${pc.dim('[Y/n] ')}`;
        }
        process.stdout.write(line);
        rl.question('', (a) => {
            rl.close();
            resolve(!/^n(o)?$/i.test(a.trim()));
        });
    });
}
function defaultAtlasUserPrompt(question) {
    return new Promise((resolve) => {
        const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
        rl.question(question, (a) => {
            rl.close();
            resolve(a);
        });
    });
}
/**
 * Determine the atlas-user for this sync.
 *
 * If `cliOverride` is set, return it immediately.
 * If ~/.claude/.atlas-user is already written, resolveAtlasUser reads it.
 * Otherwise, if atlas-toolbox is registered + has members/ dirs:
 *   - TTY: surface a readline picker + write ~/.claude/.atlas-user on pick.
 *   - non-TTY: emit stderr warning, return undefined.
 *
 * @param promptFn Injectable for tests (returns the user's selection string).
 */
async function resolveAtlasUserWithPicker(cliOverride, opts) {
    // 1. CLI override takes precedence unconditionally.
    if (cliOverride !== undefined)
        return cliOverride;
    // 2. File already written → validate, then use.
    //
    // CP3 ADV-C-1 fix: validate persisted .atlas-user against ATLAS_USER_RE
    // before trusting it. Without this, a hand-edited / corrupted /
    // stale value (e.g. admin renamed members/<name> upstream, operator
    // typed-then-fixed the file, shell-metacharacter contamination)
    // flows untouched into engine.ts where it becomes a path segment
    // and string-interpolation surface. File-resident values get the
    // same trust shape as picker-chosen ones.
    const ATLAS_USER_RE = /^[a-z0-9][a-z0-9_-]{0,38}$/;
    const claudeDirPathForRead = opts?._testClaudeDir
        ?? (process.env['OLAM_CLAUDE_DIR'] ?? path.join(os.homedir(), '.claude'));
    const atlasUserFile = path.join(claudeDirPathForRead, '.atlas-user');
    if (fs.existsSync(atlasUserFile)) {
        const existing = fs.readFileSync(atlasUserFile, 'utf-8').trim();
        if (existing.length > 0) {
            if (ATLAS_USER_RE.test(existing)) {
                return existing;
            }
            // Invalid persisted value → warn + fall through to picker (TTY)
            // or non-TTY warning. Don't return + don't throw; operator can
            // recover via picker.
            process.stderr.write(`[atlas-user-warn] ~/.claude/.atlas-user contains "${existing}" which fails validation ` +
                `(must match /^[a-z0-9][a-z0-9_-]{0,38}$/). Re-running first-sync picker.\n`);
        }
    }
    // 3. Check for atlas-toolbox source with members directories.
    const sources = listSkillSources();
    const atlasSource = sources.find((s) => s.name === 'atlas-toolbox');
    if (atlasSource === undefined)
        return undefined;
    const clonePath = skillSourceClonePath(atlasSource.id);
    const members = listMemberNames(clonePath);
    if (members.length === 0)
        return undefined;
    // 4. Non-TTY: warn and continue without atlas-user.
    const isTTY = opts?._testIsTTY ?? (process.stdout.isTTY === true);
    if (!isTTY) {
        process.stderr.write('atlas-user not configured; member overlays disabled this sync\n');
        return undefined;
    }
    // 5. TTY: show picker.
    const OTHER_OPTION = '[Other (type a new name)]';
    const choices = [...members, OTHER_OPTION];
    process.stdout.write('\n');
    process.stdout.write(pc.bold('First sync detected — pick your member name:\n'));
    choices.forEach((choice, i) => {
        process.stdout.write(`  ${pc.dim(String(i + 1) + '.')} ${choice}\n`);
    });
    process.stdout.write('\n');
    const promptFn = opts?._testPromptFn ?? defaultAtlasUserPrompt;
    const answer = (await promptFn(`Enter number or name [1-${choices.length}]: `)).trim();
    let picked;
    const num = parseInt(answer, 10);
    if (!isNaN(num) && num >= 1 && num <= choices.length) {
        const choice = choices[num - 1];
        if (choice === OTHER_OPTION) {
            // Ask for custom name
            const custom = (await promptFn('Enter new member name (lowercase, dash, digits): ')).trim();
            if (!ATLAS_USER_PICKER_RE.test(custom)) {
                process.stderr.write(`invalid name "${custom}" — must match /^[a-z0-9][a-z0-9_-]{0,38}$/. ` +
                    'Skipping atlas-user for this sync.\n');
                return undefined;
            }
            picked = custom;
        }
        else {
            picked = choice;
        }
    }
    else if (ATLAS_USER_PICKER_RE.test(answer)) {
        // Direct name entry
        picked = answer;
    }
    else {
        process.stderr.write(`invalid selection "${answer}". Skipping atlas-user for this sync.\n`);
        return undefined;
    }
    // 6. Write ~/.claude/.atlas-user.
    const claudeDirPath = opts?._testClaudeDir
        ?? (process.env['OLAM_CLAUDE_DIR'] ?? path.join(os.homedir(), '.claude'));
    fs.mkdirSync(claudeDirPath, { recursive: true });
    fs.writeFileSync(path.join(claudeDirPath, '.atlas-user'), picked + '\n', 'utf8');
    process.stdout.write(pc.green(`✓ atlas-user set to "${picked}" (written to ${path.join(claudeDirPath, '.atlas-user')})\n`));
    return picked;
}
function listDeployed() {
    const dir = claudeDir();
    const sources = listSkillSources();
    const sourcePaths = new Map(sources.map((s) => [skillSourceClonePath(s.id), s.id]));
    const entries = [];
    for (const bucket of ['skills', 'agents', 'scripts', 'rules', 'commands']) {
        const bucketDir = path.join(dir, bucket);
        if (!fs.existsSync(bucketDir))
            continue;
        for (const name of fs.readdirSync(bucketDir)) {
            const full = path.join(bucketDir, name);
            try {
                const stat = fs.lstatSync(full);
                if (!stat.isSymbolicLink())
                    continue;
                const target = fs.readlinkSync(full);
                let sourceId;
                for (const [clonePath, id] of sourcePaths.entries()) {
                    if (target.startsWith(clonePath)) {
                        sourceId = id;
                        break;
                    }
                }
                entries.push({ bucket, name, target, sourceId });
            }
            catch {
                // ignore
            }
        }
    }
    return entries;
}
function printSyncSummary(summary, dryRun) {
    printHeader(dryRun ? 'dry-run summary' : 'sync summary');
    console.log(`  sources:         ${summary.sourceCount}`);
    console.log(`  artifacts:       ${summary.artifactCount}`);
    console.log(`  hook files:      ${summary.hookFileCount}`);
    console.log(`  permission files:${summary.permissionFileCount}`);
    if (summary.deploy) {
        console.log(`  symlinks made:   ${summary.deploy.linked}`);
        if (summary.deploy.shadowBackups.length > 0) {
            console.log(`  ${pc.yellow('shadow backups:')} ${summary.deploy.shadowBackups.length}`);
            for (const p of summary.deploy.shadowBackups)
                console.log(`    ${pc.dim(p)}`);
        }
    }
    // Cross-source artifact-name collisions — surfaced for both sync + dry-run.
    // Resolution rule: first-registered source wins (mutate ordering via
    // `olam skills source reorder`).
    if (summary.collisions.length > 0) {
        console.log(`  ${pc.yellow('[conflict] name collisions:')} ${summary.collisions.length}`);
        for (const c of summary.collisions) {
            const winnerIdShort = c.winnerSourceId.slice(0, 8);
            console.log(`    ${pc.bold(`${c.bucket}/${c.basename}`)}: ${c.loserSourceIds.length + 1} sources export — using source [${winnerIdShort}]`);
            console.log(`      ${pc.dim('(winner)')} ${c.winnerSourcePath}`);
            for (let i = 0; i < c.loserSourcePaths.length; i += 1) {
                const loserIdShort = c.loserSourceIds[i].slice(0, 8);
                console.log(`      ${pc.dim(`(skipped, source ${loserIdShort})`)} ${c.loserSourcePaths[i]}`);
            }
        }
    }
    if (summary.merge) {
        console.log(`  hooks added:     ${summary.merge.hooksAdded}`);
        console.log(`  permissions:     ${summary.merge.permissionsCount}`);
        if (summary.merge.backupPath) {
            console.log(`  settings backup: ${pc.dim(summary.merge.backupPath)}`);
        }
        if (summary.merge.dualWriteDeduped > 0) {
            console.log(`  ${pc.dim(`dual-write deduped: ${summary.merge.dualWriteDeduped} (atlas-shipped hook redundant with olam-meta block)`)}`);
            // CP3 MEDIUM-1 fix: surface dropped commands so operator can see
            // exactly which entries went away. Critical when operator-forked
            // hooks share the script-name but differ in matcher/timeout.
            for (const cmd of summary.merge.dualWriteDroppedCommands) {
                console.log(`    ${pc.dim('(dropped)')} ${cmd.slice(0, 120)}${cmd.length > 120 ? '…' : ''}`);
            }
        }
    }
    // B3 — olam-meta hook injection summary
    if (summary.metaHooks) {
        const mh = summary.metaHooks;
        const memLabel = mh.servicesStatus.memory ? pc.green('up') : pc.dim('down');
        console.log(`  meta-hooks:      mode=${mh.mode} · memory=${memLabel}${mh.servicesStatus.memoryDetail ? pc.dim(` (${mh.servicesStatus.memoryDetail})`) : ''}`);
        // C3 — auto-migration line. Surfaces atlas-shipped agentmemory hook
        // entries that were stripped as part of this sync. Operator's first
        // post-merge sync prints this once; subsequent syncs are no-op
        // (idempotent — nothing to strip).
        if (mh.autoMigrated && mh.autoMigrated.strippedCount > 0) {
            console.log(`  ${pc.cyan('~ auto-migrated:')} stripped ${mh.autoMigrated.strippedCount} atlas-toolbox-shipped agentmemory hook entry(ies); replaced by olam-injected blocks`);
        }
        if (mh.blocksAdded.length > 0) {
            console.log(`  ${pc.green('+ injected:')} ${mh.blocksAdded.join(', ')}`);
        }
        if (mh.blocksRemoved.length > 0) {
            console.log(`  ${pc.yellow('- stripped:')} ${mh.blocksRemoved.join(', ')}`);
        }
        // CP3 MEDIUM-3 fix: surface snapshot-write failures so operator
        // sees that reversibility is unavailable for this sync.
        if (mh.snapshotError) {
            console.log(`  ${pc.red('! snapshot FAILED:')} ${mh.snapshotError}`);
            console.log(`  ${pc.red('  (migrate-hooks-back will not be able to undo this sync)')}`);
        }
    }
    // Phase A skill-prefix-rules: surface prefix rewrites + collisions.
    if (summary.prefixRewrites.length > 0) {
        console.log(`  prefix rewrites: ${summary.prefixRewrites.length}`);
    }
    if (summary.prefixCollisions.length > 0) {
        const allSources = summary.perSource;
        for (const col of summary.prefixCollisions) {
            const winnerName = allSources.find((s) => s.sourceId === col.winnerSourceId)?.name ?? col.winnerSourceId.slice(0, 8);
            const loserNames = col.loserSourceIds
                .map((id) => allSources.find((s) => s.sourceId === id)?.name ?? id.slice(0, 8))
                .join(', ');
            console.log(`  ${pc.yellow(`⚠ prefix collision: '${col.prefix}' used by ${winnerName} (winner) + ${loserNames}; first-registered wins`)}`);
        }
    }
    console.log();
    for (const s of summary.perSource) {
        const sub = s.fromSubscriptionsFile ? '(subscribed)' : '(all categories)';
        console.log(`  ${pc.bold(s.name.padEnd(24))} ${s.artifactCount} artifacts · ${s.categories.join(', ') || '(none)'} ${pc.dim(sub)}`);
    }
}
export function registerSkills(program) {
    const skills = program.commands.find((c) => c.name() === 'skills') ?? program.command('skills').description('Manage skill sources and synchronization');
    skills
        .command('sync')
        .description('Sync registered skill sources to ~/.claude/')
        .option('--dry-run', 'Resolve artifacts but do not deploy or merge')
        .option('--atlas-user <user>', 'Override atlas-user (defaults to ~/.claude/.atlas-user)')
        .option('--meta-hooks <mode>', 'olam-meta hook injection mode (B3): "auto" (default) — inject blocks for detected olam services; "always" — inject all blocks unconditionally; "never" — skip injection entirely', 'auto')
        .option('--with-overlays', 'after sync, run `olam skills apply-overlays` to merge ~/.claude/{skills,agents}.overrides/ over the freshly-synced upstream')
        .option('--auto-accept-source-prefix', 'ADR-021: accept a source-shipped shared/source-config.yaml prefix on FIRST sync without prompting (use for CI / scripted onboarding).')
        .option('--accept-source-prefix-changes', 'ADR-021: accept source-prefix CHANGES (when source-config.yaml differs from lastSeenSourcePrefix) without prompting.')
        .action(async (opts) => {
        try {
            // B3 — validate --meta-hooks enum (commander has no built-in enum
            // type). Reject early with clear error rather than silently using
            // engine default on bad value.
            const validModes = ['auto', 'always', 'never'];
            if (opts.metaHooks && !validModes.includes(opts.metaHooks)) {
                printError(`--meta-hooks must be one of: ${validModes.join(', ')} (got "${opts.metaHooks}")`);
                process.exitCode = 1;
                return;
            }
            const pickerHooks = globalThis.__olamTestPickerHooks;
            const atlasUser = await resolveAtlasUserWithPicker(opts.atlasUser, pickerHooks);
            // ADR-021 A10 — wire the sync-time source-prefix prompt. TTY only;
            // when no TTY AND no flag, the engine emits the skipped-non-tty
            // warning and proceeds with canonical names from that source.
            const testSyncSrcPrompt = globalThis
                .__olamTestSourcePrefixPrompt;
            const wireSyncPrompt = testSyncSrcPrompt !== undefined || Boolean(process.stdout.isTTY);
            const promptFn = testSyncSrcPrompt ?? defaultSourcePrefixSyncPrompt;
            const summary = await syncSkills({
                dryRun: opts.dryRun,
                atlasUser,
                metaHooks: (opts.metaHooks ?? 'auto'),
                autoAcceptSourcePrefix: opts.autoAcceptSourcePrefix === true,
                acceptSourcePrefixChanges: opts.acceptSourcePrefixChanges === true,
                ...(wireSyncPrompt ? { sourcePrefixPrompt: promptFn } : {}),
            });
            printSyncSummary(summary, !!opts.dryRun);
            if (!opts.dryRun) {
                printSuccess(`synced ${summary.sourceCount} source(s), ${summary.artifactCount} artifact(s)`);
            }
            if (opts.withOverlays) {
                printHeader('apply-overlays');
                const overlayResult = await applyOverlays({ dryRun: opts.dryRun });
                process.stdout.write(formatSummary(overlayResult));
                if (overlayResult.rc !== 0) {
                    process.exitCode = overlayResult.rc;
                }
            }
        }
        catch (err) {
            printError(asMessage(err));
            process.exitCode = 1;
        }
    });
    skills
        .command('diff')
        .description('Show what `olam skills sync` would deploy (no on-disk changes)')
        .option('--atlas-user <user>', 'Override atlas-user')
        .action(async (opts) => {
        try {
            const summary = await syncSkills({ dryRun: true, atlasUser: opts.atlasUser });
            printSyncSummary(summary, true);
        }
        catch (err) {
            printError(asMessage(err));
            process.exitCode = 1;
        }
    });
    skills
        .command('list')
        .description('List artifacts currently deployed to ~/.claude/')
        .action(() => {
        const entries = listDeployed();
        if (entries.length === 0) {
            console.log(pc.dim('0 deployed artifact(s). Run "olam skills sync" first.'));
            return;
        }
        const byBucket = new Map();
        for (const e of entries) {
            const list = byBucket.get(e.bucket) ?? [];
            list.push(e);
            byBucket.set(e.bucket, list);
        }
        printHeader(`${entries.length} deployed artifact(s)`);
        for (const [bucket, list] of byBucket.entries()) {
            console.log(`  ${pc.bold(bucket)} (${list.length})`);
            for (const e of list) {
                const src = e.sourceId ? `[${e.sourceId}]` : pc.yellow('[unknown]');
                console.log(`    ${e.name.padEnd(40)} ${pc.dim(src)} → ${pc.dim(e.target)}`);
            }
        }
    });
    skills
        .command('show')
        .description('Show details for a deployed artifact (by name)')
        .argument('<name>', 'Artifact name (e.g. plan-hard, codex-second-opinion.md)')
        .action((name) => {
        const entries = listDeployed();
        const matches = entries.filter((e) => e.name === name);
        if (matches.length === 0) {
            printError(`no deployed artifact matches "${name}"`);
            process.exitCode = 1;
            return;
        }
        for (const e of matches) {
            console.log(`${pc.bold('bucket:')}   ${e.bucket}`);
            console.log(`${pc.bold('name:')}     ${e.name}`);
            console.log(`${pc.bold('target:')}   ${e.target}`);
            console.log(`${pc.bold('source:')}   ${e.sourceId ?? pc.dim('(unknown — not from a registered olam source)')}`);
            console.log();
        }
    });
    skills
        .command('apply-overlays')
        .description('Merge ~/.claude/skills.overrides/ and ~/.claude/agents.overrides/ over upstream (section-as-unit merge per markdown-merger)')
        .option('--dry-run', 'Resolve merges but do not write to disk')
        .option('--fixture-root <path>', 'Override the claude root (default: ~/.claude); for tests')
        .action(async (opts) => {
        try {
            const result = await applyOverlays({
                dryRun: opts.dryRun,
                fixtureRoot: opts.fixtureRoot,
            });
            process.stdout.write(formatSummary(result));
            process.exitCode = result.rc;
        }
        catch (err) {
            printError(asMessage(err));
            process.exitCode = 2;
        }
    });
}
//# sourceMappingURL=skills.js.map