// engine/permissions-merger/merger.ts — Idempotent permission merge logic (SPEC-594)
//
// Merge semantics:
// - Allow: additive; dedup by string equality; user entries preserved
// - Deny:  always additive from template; dedup; never removed
// - defaultMode: never downgraded (bypassPermissions > acceptEdits > default)
// - NEVER touches ~/.claude/settings.json
/** Priority order for defaultMode values — higher index wins. */
const DEFAULT_MODE_PRIORITY = ['default', 'acceptEdits', 'bypassPermissions'];
function modePriority(mode) {
    const idx = DEFAULT_MODE_PRIORITY.indexOf(mode);
    return idx === -1 ? -1 : idx;
}
/**
 * Returns true if `candidate` would be an upgrade over `current`.
 * 'bypassPermissions' > 'acceptEdits' > 'default'.
 * Unknown modes are treated as lowest priority.
 */
export function shouldUpgradeDefaultMode(current, candidate) {
    return modePriority(candidate) > modePriority(current);
}
/** Deduplicate an array of strings while preserving order of first occurrence. */
export function dedupeRules(rules) {
    const seen = new Set();
    const result = [];
    for (const r of rules) {
        if (!seen.has(r)) {
            seen.add(r);
            result.push(r);
        }
    }
    return result;
}
/**
 * Compute a PermissionMergePlan (what would change) without modifying anything.
 *
 * @param existing - Current ClaudeCodeSettings (may have empty/missing permissions)
 * @param template - The target template for the chosen mode
 * @param extraDeny - Additional deny rules from stack detection
 */
export function planMerge(existing, template, extraDeny) {
    const existingAllow = existing.permissions?.allow ?? [];
    const existingDeny = existing.permissions?.deny ?? [];
    const existingMode = existing.permissions?.defaultMode;
    const allTemplateAllow = dedupeRules(template.allow);
    const allTemplateDeny = dedupeRules([...template.deny, ...extraDeny]);
    const existingAllowSet = new Set(existingAllow);
    const existingDenySet = new Set(existingDeny);
    const allowToAdd = allTemplateAllow.filter((r) => !existingAllowSet.has(r));
    const allowAlreadyPresent = allTemplateAllow.filter((r) => existingAllowSet.has(r));
    const denyToAdd = allTemplateDeny.filter((r) => !existingDenySet.has(r));
    const denyAlreadyPresent = allTemplateDeny.filter((r) => existingDenySet.has(r));
    let defaultModeAction;
    let defaultModeValue;
    if (existingMode === undefined || existingMode === '') {
        defaultModeAction = 'set';
        defaultModeValue = template.defaultMode;
    }
    else if (shouldUpgradeDefaultMode(existingMode, template.defaultMode)) {
        defaultModeAction = 'upgrade';
        defaultModeValue = template.defaultMode;
    }
    else {
        defaultModeAction = 'keep';
        defaultModeValue = existingMode;
    }
    return {
        allowToAdd,
        allowAlreadyPresent,
        denyToAdd,
        denyAlreadyPresent,
        defaultModeAction,
        defaultModeValue,
    };
}
/**
 * Apply a PermissionMergePlan to an existing ClaudeCodeSettings object, returning the merged result.
 * The input object is NOT mutated — a new object is returned.
 */
export function applyPlan(existing, plan) {
    const existingAllow = existing.permissions?.allow ?? [];
    const existingDeny = existing.permissions?.deny ?? [];
    const newAllow = dedupeRules([...existingAllow, ...plan.allowToAdd]);
    const newDeny = dedupeRules([...existingDeny, ...plan.denyToAdd]);
    const newMode = plan.defaultModeAction === 'set' || plan.defaultModeAction === 'upgrade'
        ? (plan.defaultModeValue ?? undefined)
        : existing.permissions?.defaultMode;
    return {
        ...existing,
        permissions: {
            ...existing.permissions,
            allow: newAllow,
            deny: newDeny,
            ...(newMode !== undefined ? { defaultMode: newMode } : {}),
        },
    };
}
/**
 * Idempotent merge: compute plan and (if changes needed) apply it.
 *
 * @param existing - Current settings object (may be empty `{}`)
 * @param template - Template for the chosen mode
 * @param extraDeny - Stack-specific deny rules to add
 */
export function mergePermissions(existing, template, extraDeny) {
    const plan = planMerge(existing, template, extraDeny);
    const hasChanges = plan.allowToAdd.length > 0 ||
        plan.denyToAdd.length > 0 ||
        plan.defaultModeAction === 'set' ||
        plan.defaultModeAction === 'upgrade';
    const final = hasChanges ? applyPlan(existing, plan) : existing;
    return {
        modified: hasChanges,
        plan,
        final,
    };
}
//# sourceMappingURL=merger.js.map