import fsp from 'node:fs/promises';
import path from 'node:path';
import { runProcess, writeTextAtomic } from '../fsx.js';
import { isMutationAllowed, isPathAllowed } from './requested-scope-contract.js';
import { evaluateMutation, recordMutation } from './mutation-ledger.js';
// Mutation Guard Adapter (1.20.2 Area 1a).
//
// Thin wrappers that force every genuinely-risky mutation through the existing
// Requested-Scope-Contract + Mutation-Ledger before it touches the filesystem or
// spawns a process. This layer adds NO new IO primitives and NO new scope/ledger
// logic — it composes `isMutationAllowed`/`evaluateMutation`/`recordMutation` from
// requested-scope-contract.ts + mutation-ledger.ts and delegates the actual op to
// fsx/fsp/runProcess. A scope violation throws BEFORE the mutation is applied; a
// config/skill mutation lacking a backup or no-op reason throws; every applied
// mutation is recorded to the ledger.
export const MUTATION_GUARD_SCHEMA = 'sks.mutation-guard.v1';
export class MutationGuardViolationError extends Error {
    kind;
    target;
    reason;
    constructor(kind, target, reason) {
        super(`mutation_guard_violation:${kind}:${reason}:${target}`);
        this.name = 'MutationGuardViolationError';
        this.kind = kind;
        this.target = target;
        this.reason = reason;
    }
}
// Config/skill mutations that must carry a backup or a no-op reason (mirrors the
// `needsBackup` rule inside evaluateMutation so the guard can pre-validate).
const NEEDS_BACKUP = new Set([
    'global_config_write',
    'codex_app_flag_change',
    'codex_lb_auth_change',
    'skill_snapshot_promotion'
]);
/**
 * Core guard: validate scope + backup BEFORE applying, run `apply`, then record
 * the mutation to the ledger. Throws MutationGuardViolationError without applying
 * when the mutation is out of scope or a backup-requiring mutation has neither a
 * backup nor a no-op reason. On apply failure, records `applied:false` + reason
 * and rethrows the underlying error.
 */
async function guard(ctx, kind, target, opts, apply) {
    const evalOpts = buildEvalOpts(target, opts, false);
    // Pre-apply scope check (applied:false → violation reflects scope only).
    const scope = isMutationAllowed(ctx.contract, scopeForKind(kind), { confirmed: opts.confirmed === true });
    if (!scope.allowed)
        throw new MutationGuardViolationError(kind, target, scope.reason);
    for (const pathTarget of pathTargetsFor(kind, target, opts)) {
        const pathDecision = isPathAllowed(ctx.contract, pathTarget);
        if (!pathDecision.allowed)
            throw new MutationGuardViolationError(kind, pathTarget, pathDecision.reason);
    }
    if (NEEDS_BACKUP.has(kind) && !opts.backupPath && !opts.noOpReason) {
        throw new MutationGuardViolationError(kind, target, 'backup_or_no_op_reason_required');
    }
    void evalOpts;
    try {
        const result = await apply();
        const entry = evaluateMutation(ctx.contract, kind, buildEvalOpts(target, opts, true));
        await recordMutation(ctx.root, entry);
        if (entry.violation)
            throw new MutationGuardViolationError(kind, target, entry.reason || 'post_apply_violation');
        return result;
    }
    catch (err) {
        if (err instanceof MutationGuardViolationError)
            throw err;
        const failed = evaluateMutation(ctx.contract, kind, { ...buildEvalOpts(target, opts, false), noOpReason: `apply_failed:${err?.message || String(err)}` });
        await recordMutation(ctx.root, failed).catch(() => { });
        throw err;
    }
}
function scopeForKind(kind) {
    // Reuse the ledger's KIND_TO_SCOPE indirectly: evaluateMutation already maps it,
    // but the pre-apply scope check needs the contract scope directly. Mirror the map.
    switch (kind) {
        case 'global_config_write':
        case 'codex_app_flag_change':
            return 'global_codex_config';
        case 'package_install':
            return 'package_install';
        case 'process_kill':
            return 'codex_app_process';
        case 'codex_lb_auth_change':
            return 'codex_lb_auth';
        case 'zellij_install':
            return 'zellij_install';
        case 'skill_snapshot_promotion':
            return 'skill_snapshot_promotion';
        default:
            return 'project_files';
    }
}
function buildEvalOpts(target, opts, applied) {
    const out = { target, applied };
    if (opts.confirmed !== undefined)
        out.confirmed = opts.confirmed;
    if (opts.backupPath !== undefined)
        out.backupPath = opts.backupPath;
    if (opts.noOpReason !== undefined)
        out.noOpReason = opts.noOpReason;
    return out;
}
const PATH_SCOPED_KINDS = new Set([
    'file_write',
    'file_delete',
    'file_rename',
    'chmod',
    'xattr',
    'chflags',
    'global_config_write'
]);
function pathTargetsFor(kind, target, opts) {
    if (!PATH_SCOPED_KINDS.has(kind))
        return [];
    if (opts.pathTargets?.length)
        return opts.pathTargets;
    if (kind === 'file_rename' && target.includes(' -> '))
        return target.split(' -> ').map((s) => s.trim()).filter(Boolean);
    return [target];
}
// ---- Public guarded wrappers -------------------------------------------------
export async function guardedWriteFile(ctx, target, data, opts = {}) {
    await guard(ctx, 'file_write', target, opts, () => writeTextAtomic(target, data));
}
export async function guardedGlobalCodexConfigWrite(ctx, target, data, opts = {}) {
    await guard(ctx, 'global_config_write', target, opts, () => writeTextAtomic(target, data));
}
export async function guardedRm(ctx, target, opts = {}) {
    await guard(ctx, 'file_delete', target, opts, () => fsp.rm(target, { recursive: opts.recursive === true, force: opts.force !== false }));
}
export async function guardedRename(ctx, from, to, opts = {}) {
    await guard(ctx, 'file_rename', `${from} -> ${to}`, { ...opts, pathTargets: [from, to] }, () => fsp.rename(from, to));
}
export async function guardedChmod(ctx, target, mode, opts = {}) {
    await guard(ctx, 'chmod', target, opts, () => fsp.chmod(target, mode));
}
export async function guardedChflags(ctx, target, flag, opts = {}) {
    await guard(ctx, 'chflags', target, opts, async () => {
        const result = await runProcess('chflags', [flag, target], { timeoutMs: 5000 });
        if (result.code !== 0)
            throw new Error(`chflags_failed:${result.stderr || result.code}`);
    });
}
export async function guardedXattr(ctx, target, action, opts = {}) {
    await guard(ctx, 'xattr', target, opts, async () => {
        const args = action.op === 'remove' ? ['-d', action.name, target] : ['-w', action.name, action.value, target];
        const result = await runProcess('xattr', args, { timeoutMs: 5000 });
        // xattr -d returns 1 when the attribute is already absent; treat as no-op.
        if (result.code !== 0 && !(action.op === 'remove' && result.code === 1)) {
            throw new Error(`xattr_failed:${result.stderr || result.code}`);
        }
    });
}
export async function guardedProcessKill(ctx, pid, opts = {}) {
    await guard(ctx, 'process_kill', `pid:${pid}`, opts, async () => {
        process.kill(pid, opts.signal ?? 'SIGTERM');
    });
}
export async function guardedPackageInstall(ctx, spec, opts) {
    return guard(ctx, 'package_install', spec, opts, async () => {
        const runOpts = { timeoutMs: opts.timeoutMs ?? 600000 };
        if (opts.cwd !== undefined)
            runOpts.cwd = opts.cwd;
        if (opts.env !== undefined)
            runOpts.env = opts.env;
        if (opts.maxOutputBytes !== undefined)
            runOpts.maxOutputBytes = opts.maxOutputBytes;
        const result = await runProcess(opts.command, opts.args, runOpts);
        return { code: result.code, stdout: result.stdout, stderr: result.stderr };
    });
}
export async function guardedSkillSnapshotPromotion(ctx, skillTarget, opts, apply) {
    return guard(ctx, 'skill_snapshot_promotion', skillTarget, opts, apply);
}
// Escape hatch for callers that already have an atomic apply but want guarding:
export async function guardedApply(ctx, kind, target, opts, apply) {
    return guard(ctx, kind, target, opts, apply);
}
export function guardContextForRoute(root, contract, reason) {
    const ctx = { root: path.resolve(root), contract };
    if (reason !== undefined)
        ctx.reason = reason;
    return ctx;
}
//# sourceMappingURL=mutation-guard.js.map