/**
 * commands/worktree-install.mjs — language-agnostic worktree bootstrap (C54).
 *
 * Subcommand shape:
 *   apt-tools worktree-install <project-dir>
 *       --task <task-id>
 *       [--worktree <path>]
 *       [--no-bootstrap]         (skip entirely, write install_status: "skipped")
 *
 * Finalize mode (called by the child wrapper after install exits):
 *   apt-tools worktree-install <project-dir>
 *       --finalize
 *       --task <task-id>
 *       --ecosystem <eco>
 *       --exit-code <n>
 *
 * Main-mode flow:
 *   1. Resolve worktree path + task state.
 *   2. Check escape hatches (--no-bootstrap, skip_bootstrap_for glob,
 *      config.bootstrap_worktree false, existing install_status running).
 *   3. Detect ecosystem(s) at the worktree path.
 *   4. Run manifest-scanner (URL allowlist + multi-lockfile).
 *   5. Acquire a semaphore slot (returns queued on cap).
 *   6. Build stubbed HOME for the task.
 *   7. Write wrapper script + spawn detached child per ecosystem.
 *   8. Write install_status: "running" + install_pid + etc. to state.json
 *      SYNCHRONOUSLY before spawn. Emit install.started.
 *   9. Return JSON envelope synchronously (never blocks on install).
 *
 * Finalize-mode flow:
 *   1. Read state.json entry for task + ecosystem.
 *   2. Mark install_status "ok" (exit 0) or "failed" (non-zero).
 *   3. Release semaphore slot.
 *   4. Emit install.ok / install.failed.
 */
import { existsSync, mkdirSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { getTaskIsolationConfig } from '../config/load.mjs';
import { createCoordinationStore } from '../coordination/store.mjs';
import { spawnDetachedInstall, writeWrapperScript } from '../install/detached-spawn.mjs';
import { getInstallCommand } from '../install/ecosystem-commands.mjs';
import { buildStubbedHome } from '../install/home-stub.mjs';
import { acquireInstallSlot, releaseInstallSlot } from '../install/install-semaphore.mjs';
import { scanManifests } from '../install/manifest-scanner.mjs';
import { parseFlags } from '../util/args.mjs';
import { resolveAptToolsBin } from '../util/bin-path.mjs';
import { err, ok, UsageError } from '../util/result.mjs';
import { detectEcosystems } from './toolchain-detect.mjs';
/**
 * Sentinel error class for helper-level usage failures inside cmdWorktreeInstall.
 * The dispatcher catches it and converts it into the standard err() envelope.
 */
class WorktreeInstallUsageError extends UsageError {
    constructor(message) {
        super(message);
        this.name = 'WorktreeInstallUsageError';
    }
}
const SAFE_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
/**
 * Minimal glob-matcher for `skip_bootstrap_for`. Supports `*` and `**`
 * segment wildcards only — same subset the rest of the framework uses.
 *
 * @param {string} glob
 * @param {string} target
 * @returns {boolean}
 */
function matchGlob(glob, target) {
    // Build the regex in three passes to avoid escape-and-replace ordering
    // foot-guns. ** and * are swapped for control-char placeholders BEFORE
    // the generic regex-metachar escape so neither placeholder leaks through
    // the escape pass. U+0001/U+0002/U+0003 never appear in a valid path.
    //
    // Semantic quirk: `dir/**` matches `dir` itself (convention from glob /
    // minimatch). Collapse a trailing /** first so the bare-dir case works.
    const DOUBLESTAR = String.fromCharCode(1);
    const STAR = String.fromCharCode(2);
    const TRAILING_DOUBLESTAR = String.fromCharCode(3);
    let body = glob;
    if (body.endsWith('/**'))
        body = body.slice(0, -3) + TRAILING_DOUBLESTAR;
    const pattern = body
        .replace(/\*\*/g, DOUBLESTAR)
        .replace(/\*/g, STAR)
        .replace(/[.+?^${}()|[\]\\]/g, '\\$&')
        .replace(new RegExp(DOUBLESTAR, 'g'), '.*')
        .replace(new RegExp(STAR, 'g'), '[^/]*')
        .replace(new RegExp(TRAILING_DOUBLESTAR, 'g'), '(?:/.*)?');
    const re = new RegExp(`^${pattern}$`);
    return re.test(target);
}
/**
 * Main-mode entry. Synchronous return — spawns detached children and
 * returns the install state so the caller (worktree create) can merge
 * it into state.json.
 *
 * @param {string} projectDir
 * @param {string[]} extraArgs
 */
export function cmdWorktreeInstall(projectDir, extraArgs = []) {
    if (!projectDir) {
        return err('Usage: apt-tools worktree-install <project-dir> --task <task-id> [--worktree <path>] [--no-bootstrap]   OR   --finalize --task <id> --ecosystem <eco> --exit-code <n>');
    }
    const targetDir = resolve(projectDir);
    const flags = parseFlags(extraArgs);
    try {
        // --- Finalize mode: called by the wrapper-script at install child's end ---
        if (flags.has('finalize')) {
            return finalizeInstall(targetDir, flags);
        }
        return runMainInstall(targetDir, flags);
    }
    catch (e) {
        if (e instanceof WorktreeInstallUsageError)
            return err(e.message);
        throw e;
    }
}
/**
 * Main-mode install execution — extracted from cmdWorktreeInstall so the
 * sentinel-error try/catch wraps a single call site.
 *
 * @param {string} targetDir
 * @param {ReturnType<typeof parseFlags>} flags
 */
function runMainInstall(targetDir, flags) {
    const taskId = flags.get('task');
    if (!taskId)
        throw new WorktreeInstallUsageError('Required: --task <task-id>');
    if (!SAFE_ID_PATTERN.test(taskId))
        throw new WorktreeInstallUsageError(`Invalid task id: ${taskId}`);
    const noBootstrap = flags.has('no-bootstrap');
    const worktreeArg = flags.get('worktree');
    const store = createCoordinationStore(targetDir);
    const state = store.readState();
    const task = state?.active_tasks?.[taskId];
    if (!task) {
        return ok({
            status: 'ok',
            command: 'worktree-install',
            task_id: taskId,
            install_status: 'skipped',
            install_warning: 'task not found in state.active_tasks',
        });
    }
    const worktreePath = typeof worktreeArg === 'string' && worktreeArg
        ? resolve(targetDir, worktreeArg)
        : task.worktree_path;
    if (!worktreePath || !existsSync(worktreePath)) {
        const payload = {
            status: 'ok',
            command: 'worktree-install',
            task_id: taskId,
            install_status: 'skipped',
            install_warning: `worktree path not found: ${worktreePath || '(unset)'}`,
        };
        persistSkip(store, taskId, payload.install_warning);
        return ok(payload);
    }
    // --- Escape hatches ---
    const isolation = getTaskIsolationConfig(targetDir);
    if (noBootstrap) {
        const payload = {
            status: 'ok',
            command: 'worktree-install',
            task_id: taskId,
            install_status: 'skipped',
            install_warning: 'skipped via --no-bootstrap flag',
        };
        persistSkip(store, taskId, payload.install_warning);
        return ok(payload);
    }
    if (isolation.bootstrap_worktree !== true) {
        const payload = {
            status: 'ok',
            command: 'worktree-install',
            task_id: taskId,
            install_status: 'skipped',
            install_warning: 'bootstrap_worktree disabled (task_isolation.bootstrap_worktree is not true)',
        };
        persistSkip(store, taskId, payload.install_warning);
        return ok(payload);
    }
    const skipGlobs = Array.isArray(isolation.skip_bootstrap_for) ? isolation.skip_bootstrap_for : [];
    if (skipGlobs.length > 0) {
        const rel = worktreePath.startsWith(targetDir)
            ? worktreePath.slice(targetDir.length + 1)
            : worktreePath;
        for (const g of skipGlobs) {
            if (typeof g === 'string' && matchGlob(g, rel)) {
                const payload = {
                    status: 'ok',
                    command: 'worktree-install',
                    task_id: taskId,
                    install_status: 'skipped',
                    install_warning: `skipped by config.skip_bootstrap_for: ${g}`,
                };
                persistSkip(store, taskId, payload.install_warning);
                return ok(payload);
            }
        }
    }
    // --- Ecosystem detection ---
    const ecosystems = detectEcosystems(worktreePath);
    if (ecosystems.length === 0) {
        const payload = {
            status: 'ok',
            command: 'worktree-install',
            task_id: taskId,
            install_status: 'skipped',
            install_ecosystems: [],
            install_warning: 'no ecosystems detected at worktree path',
        };
        persistSkip(store, taskId, payload.install_warning);
        return ok(payload);
    }
    // --- Manifest scan ---
    const scan = scanManifests(worktreePath, ecosystems, {
        allowlist: isolation.install_registry_allowlist,
    });
    if (!scan.ok) {
        const warning = scan.rejections.slice(0, 3).join('; ');
        const payload = {
            status: 'ok',
            command: 'worktree-install',
            task_id: taskId,
            install_status: 'failed',
            install_ecosystems: ecosystems.map((e) => e.ecosystem),
            install_warning: warning,
            install_rejections: scan.rejections,
        };
        persistFailPreSpawn(store, taskId, payload);
        // Emit install.failed as well — but do it via the store seam to stay
        // append-only. Best-effort.
        try {
            for (const eco of payload.install_ecosystems) {
                store.appendEvent({
                    op: 'install.failed',
                    task: taskId,
                    data: {
                        task_id: taskId,
                        ecosystem: eco,
                        exit_code: null,
                        duration_ms: 0,
                        reason: warning,
                        log_path: null,
                    },
                });
            }
        }
        catch {
            /* best-effort */
        }
        return ok(payload);
    }
    // --- Spawn per-ecosystem install child ---
    const taskInstallDir = join(targetDir, '.aperant', 'tasks', taskId, 'install');
    mkdirSync(taskInstallDir, { recursive: true });
    const { homeDir } = buildStubbedHome({ taskId, projectDir: targetDir });
    const installRecord = {
        install_status: 'running',
        install_started_at: new Date().toISOString(),
        install_finished_at: null,
        install_pid: null,
        install_ecosystems: ecosystems.map((e) => e.ecosystem),
        install_log_path: taskInstallDir,
        install_warning: null,
        install_per_ecosystem: {},
    };
    // Resolve the worktree-install command path for the wrapper's --finalize call.
    // D-03: delegated to resolveAptToolsBin() which centralizes the 5-step
    // fallback (env override → process.argv[1] → monorepo → consumer-install
    // → $PATH). This file no longer reads process.argv[1] directly.
    const aptToolsPath = resolveAptToolsBin(targetDir);
    // Custom bootstrap_cmd overrides per-ecosystem detection. When set, we run
    // the single command and treat it as one "virtual ecosystem" entry.
    const bootstrapCmd = isolation.bootstrap_cmd;
    if (Array.isArray(bootstrapCmd) && bootstrapCmd.length > 0) {
        spawnOneEcosystem({
            ecosystemName: 'custom',
            argv: bootstrapCmd,
            shellWrapper: null,
            worktreePath,
            taskInstallDir,
            taskId,
            homeDir,
            aptToolsPath,
            targetDir,
            projectDir: targetDir,
            isolation,
            store,
            installRecord,
        });
    }
    else {
        for (const eco of ecosystems) {
            const cmdEntry = getInstallCommand(eco.ecosystem);
            if (!cmdEntry) {
                installRecord.install_per_ecosystem[eco.ecosystem] = {
                    status: 'skipped',
                    reason: 'no canonical install command registered',
                };
                continue;
            }
            spawnOneEcosystem({
                ecosystemName: eco.ecosystem,
                argv: cmdEntry.cmd,
                shellWrapper: cmdEntry.shell_wrapper || null,
                worktreePath,
                taskInstallDir,
                taskId,
                homeDir,
                aptToolsPath,
                targetDir,
                projectDir: targetDir,
                isolation,
                store,
                installRecord,
            });
        }
    }
    // Persist the install record on the task.
    store.updateState((s) => {
        if (!s?.active_tasks?.[taskId])
            return s;
        Object.assign(s.active_tasks[taskId], {
            install_status: installRecord.install_status,
            install_started_at: installRecord.install_started_at,
            install_finished_at: installRecord.install_finished_at,
            install_pid: installRecord.install_pid,
            install_ecosystems: installRecord.install_ecosystems,
            install_log_path: installRecord.install_log_path,
            install_warning: installRecord.install_warning,
            install_per_ecosystem: installRecord.install_per_ecosystem,
        });
        s.last_activity = new Date().toISOString();
        return s;
    });
    return ok({
        status: 'ok',
        command: 'worktree-install',
        task_id: taskId,
        ...installRecord,
    });
}
/**
 * Helper — acquire slot, write wrapper script, spawn. Mutates installRecord
 * + emits install.started event.
 */
function spawnOneEcosystem({ ecosystemName, argv, shellWrapper, worktreePath, taskInstallDir, taskId, homeDir, aptToolsPath, targetDir, projectDir, isolation, store, installRecord, }) {
    // Semaphore first — if queued, we still record the intent.
    const max = typeof isolation.max_parallel_installs === 'number' ? isolation.max_parallel_installs : 2;
    const slot = acquireInstallSlot({
        projectDir: targetDir,
        max,
        taskId,
        ecosystem: ecosystemName,
    });
    if (!slot.acquired) {
        installRecord.install_status = 'queued';
        installRecord.install_warning = `queued (${slot.live_count} running, max=${max})`;
        installRecord.install_per_ecosystem[ecosystemName] = {
            status: 'queued',
            queue_position: slot.queue_position,
        };
        return;
    }
    const logPath = join(taskInstallDir, `${ecosystemName}.log`);
    const scriptPath = join(taskInstallDir, `${ecosystemName}-wrapper.sh`);
    try {
        writeWrapperScript({
            scriptPath,
            cwd: worktreePath,
            cmd: argv,
            aptToolsPath,
            taskId,
            ecosystem: ecosystemName,
            projectDir,
            shellWrapper,
        });
        const spawned = spawnDetachedInstall({
            argv,
            cwd: worktreePath,
            logPath,
            homeDir,
            ecosystem: ecosystemName,
            wrapperPath: scriptPath,
        });
        installRecord.install_pid = spawned.pid;
        installRecord.install_per_ecosystem[ecosystemName] = {
            status: 'running',
            pid: spawned.pid,
            started_at: spawned.started_at,
            log_path: logPath,
        };
        try {
            store.appendEvent({
                op: 'install.started',
                task: taskId,
                data: {
                    task_id: taskId,
                    ecosystem: ecosystemName,
                    pid: spawned.pid,
                    started_at: spawned.started_at,
                    log_path: logPath,
                },
            });
        }
        catch {
            /* best-effort */
        }
    }
    catch (err) {
        installRecord.install_status = 'failed';
        installRecord.install_warning = `spawn failed: ${err.message}`;
        installRecord.install_per_ecosystem[ecosystemName] = {
            status: 'failed',
            reason: err.message,
        };
        // Release the semaphore slot since we failed to spawn.
        if (slot.released_by)
            slot.released_by();
    }
}
/**
 * Finalize mode: the wrapper script calls back in after the install
 * command exits. We rewrite install_status based on the exit code and
 * release the semaphore slot.
 */
function finalizeInstall(targetDir, flags) {
    const taskId = flags.get('task');
    const ecosystem = flags.get('ecosystem');
    const exitCodeRaw = flags.get('exit-code');
    if (!taskId || !ecosystem) {
        throw new WorktreeInstallUsageError('worktree-install --finalize requires --task <id> and --ecosystem <eco>');
    }
    const exitCode = parseInt(String(exitCodeRaw ?? '1'), 10);
    const finalizeOk = exitCode === 0;
    const store = createCoordinationStore(targetDir);
    let durationMs = 0;
    let logPath = null;
    store.updateState((state) => {
        if (!state?.active_tasks?.[taskId])
            return state;
        const t = state.active_tasks[taskId];
        if (!t.install_per_ecosystem)
            t.install_per_ecosystem = {};
        const perEco = t.install_per_ecosystem;
        const record = perEco[ecosystem] || {};
        const finishedAt = new Date();
        if (record.started_at) {
            durationMs = finishedAt.getTime() - new Date(record.started_at).getTime();
        }
        logPath = record.log_path || null;
        record.status = finalizeOk ? 'ok' : 'failed';
        record.finished_at = finishedAt.toISOString();
        record.exit_code = exitCode;
        record.duration_ms = durationMs;
        perEco[ecosystem] = record;
        // Roll up per-ecosystem to the task-level install_status:
        //   - any 'failed' ⇒ failed
        //   - any 'running'/'queued' ⇒ running
        //   - all 'ok' ⇒ ok
        const vals = Object.values(perEco);
        const anyFailed = vals.some((r) => r?.status === 'failed');
        const anyInFlight = vals.some((r) => r?.status === 'running' || r?.status === 'queued');
        if (anyFailed)
            t.install_status = 'failed';
        else if (anyInFlight)
            t.install_status = 'running';
        else
            t.install_status = 'ok';
        if (!anyInFlight)
            t.install_finished_at = finishedAt.toISOString();
        if (anyFailed)
            t.install_warning = `install failed (${ecosystem} exit=${exitCode})`;
        state.last_activity = finishedAt.toISOString();
        return state;
    });
    // Release the semaphore slot.
    releaseInstallSlot({ projectDir: targetDir, taskId, ecosystem });
    // Emit install.ok / install.failed.
    try {
        store.appendEvent({
            op: finalizeOk ? 'install.ok' : 'install.failed',
            task: taskId,
            data: {
                task_id: taskId,
                ecosystem,
                exit_code: exitCode,
                duration_ms: durationMs,
                log_path: logPath,
                ...(finalizeOk ? {} : { reason: `exit ${exitCode}` }),
            },
        });
    }
    catch {
        /* best-effort */
    }
    return ok({
        status: 'ok',
        command: 'worktree-install-finalize',
        task_id: taskId,
        ecosystem,
        exit_code: exitCode,
        result: finalizeOk ? 'ok' : 'failed',
        duration_ms: durationMs,
    });
}
/**
 * Persist a "skipped" install record on the task.
 */
function persistSkip(store, taskId, warning) {
    try {
        store.updateState((state) => {
            if (!state?.active_tasks?.[taskId])
                return state;
            const t = state.active_tasks[taskId];
            t.install_status = 'skipped';
            t.install_warning = warning;
            t.install_finished_at = new Date().toISOString();
            state.last_activity = new Date().toISOString();
            return state;
        });
    }
    catch {
        /* best-effort — never abort on state-write failure here */
    }
}
/**
 * Persist a pre-spawn "failed" record (multi-lockfile or URL rejection).
 */
function persistFailPreSpawn(store, taskId, payload) {
    try {
        store.updateState((state) => {
            if (!state?.active_tasks?.[taskId])
                return state;
            const t = state.active_tasks[taskId];
            t.install_status = 'failed';
            t.install_warning = payload.install_warning;
            t.install_ecosystems = payload.install_ecosystems;
            t.install_rejections = payload.install_rejections;
            t.install_finished_at = new Date().toISOString();
            state.last_activity = new Date().toISOString();
            return state;
        });
    }
    catch {
        /* best-effort */
    }
}
//# sourceMappingURL=worktree-install.mjs.map