/**
 * NERF Preferences -- loading, merging, and rendering.
 *
 * This module is the primary entry point for preference operations.
 * Type definitions live in ./preferences-types.js, validation in
 * ./preferences-validation.js, skill logic in ./preferences-skills.js,
 * and model logic in ./preferences-models.js.
 *
 * All symbols are re-exported here so that existing `import { ... } from "./preferences.js"`
 * statements continue to work without modification.
 */
import { existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { nerfRoot } from "./paths.js";
import { parse as parseYaml } from "yaml";
import { normalizeStringArray } from "../shared/format-utils.js";
import { resolveProfileDefaults as _resolveProfileDefaults } from "./preferences-models.js";
import { KNOWN_PREFERENCE_KEYS, MODE_DEFAULTS, } from "./preferences-types.js";
import { validatePreferences } from "./preferences-validation.js";
import { formatSkillRef } from "./preferences-skills.js";
// ─── Re-exports: validation ─────────────────────────────────────────────────
export { validatePreferences } from "./preferences-validation.js";
// ─── Re-exports: skills ─────────────────────────────────────────────────────
export { resolveAllSkillReferences, resolveSkillDiscoveryMode, resolveSkillStalenessDays, } from "./preferences-skills.js";
// ─── Re-exports: models ─────────────────────────────────────────────────────
export { resolveModelForUnit, resolveModelWithFallbacksForUnit, getNextFallbackModel, validateModelId, updatePreferencesModels, resolveDynamicRoutingConfig, resolveAutoSupervisorConfig, resolveProfileDefaults, resolveEffectiveProfile, resolveInlineLevel, resolveContextSelection, resolveSearchProviderFromPreferences, } from "./preferences-models.js";
// ─── Path Constants & Getters ───────────────────────────────────────────────
function nerfHome() {
    return process.env.NERF_HOME || join(homedir(), ".nerf");
}
function globalPreferencesPath() {
    return join(nerfHome(), "PREFERENCES.md");
}
function legacyGlobalPreferencesPath() {
    return join(homedir(), ".pi", "agent", "nerf-preferences.md");
}
function projectPreferencesPath() {
    return join(nerfRoot(process.cwd()), "PREFERENCES.md");
}
// Legacy: older versions used lowercase preferences.md.
// Check lowercase as a fallback so those files aren't silently ignored.
function globalPreferencesPathLegacy() {
    return join(nerfHome(), "preferences.md");
}
function projectPreferencesPathLegacy() {
    return join(nerfRoot(process.cwd()), "preferences.md");
}
export function getGlobalNerfPreferencesPath() {
    return globalPreferencesPath();
}
export function getLegacyGlobalNerfPreferencesPath() {
    return legacyGlobalPreferencesPath();
}
export function getProjectNerfPreferencesPath() {
    return projectPreferencesPath();
}
// ─── Loading ────────────────────────────────────────────────────────────────
export function loadGlobalNerfPreferences() {
    return loadPreferencesFile(globalPreferencesPath(), "global")
        ?? loadPreferencesFile(globalPreferencesPathLegacy(), "global")
        ?? loadPreferencesFile(legacyGlobalPreferencesPath(), "global");
}
export function loadProjectNerfPreferences() {
    return loadPreferencesFile(projectPreferencesPath(), "project")
        ?? loadPreferencesFile(projectPreferencesPathLegacy(), "project");
}
export function loadEffectiveNerfPreferences() {
    const globalPreferences = loadGlobalNerfPreferences();
    const projectPreferences = loadProjectNerfPreferences();
    if (!globalPreferences && !projectPreferences)
        return null;
    let result;
    if (!globalPreferences) {
        result = projectPreferences;
    }
    else if (!projectPreferences) {
        result = globalPreferences;
    }
    else {
        const mergedWarnings = [
            ...(globalPreferences.warnings ?? []),
            ...(projectPreferences.warnings ?? []),
        ];
        result = {
            path: projectPreferences.path,
            scope: "project",
            preferences: mergePreferences(globalPreferences.preferences, projectPreferences.preferences),
            ...(mergedWarnings.length > 0 ? { warnings: mergedWarnings } : {}),
        };
    }
    // Apply token-profile defaults as the lowest-priority layer so that
    // `token_profile: budget` sets models and phase-skips automatically.
    // Explicit user preferences always override profile defaults.
    const profile = result.preferences.token_profile;
    if (profile) {
        const profileDefaults = _resolveProfileDefaults(profile);
        result = {
            ...result,
            preferences: mergePreferences(profileDefaults, result.preferences),
        };
    }
    // Apply mode defaults as the lowest-priority layer
    if (result.preferences.mode) {
        result = {
            ...result,
            preferences: applyModeDefaults(result.preferences.mode, result.preferences),
        };
    }
    return result;
}
function loadPreferencesFile(path, scope) {
    if (!existsSync(path))
        return null;
    const raw = readFileSync(path, "utf-8");
    const preferences = parsePreferencesMarkdown(raw);
    if (!preferences)
        return null;
    const validation = validatePreferences(preferences);
    const allWarnings = [...validation.warnings, ...validation.errors];
    return {
        path,
        scope,
        preferences: validation.preferences,
        ...(allWarnings.length > 0 ? { warnings: allWarnings } : {}),
    };
}
let _warnedUnrecognizedFormat = false;
/** @internal Reset the warn-once flag — exported for testing only. */
export function _resetParseWarningFlag() {
    _warnedUnrecognizedFormat = false;
}
/** @internal Exported for testing only */
export function parsePreferencesMarkdown(content) {
    // Use indexOf instead of [\s\S]*? regex to avoid backtracking (#468)
    const startMarker = content.startsWith('---\r\n') ? '---\r\n' : '---\n';
    if (content.startsWith(startMarker)) {
        const searchStart = startMarker.length;
        const endIdx = content.indexOf('\n---', searchStart);
        if (endIdx === -1)
            return null;
        const block = content.slice(searchStart, endIdx);
        return parseFrontmatterBlock(block.replace(/\r/g, ''));
    }
    // Fallback: heading+list format (e.g. "## Git\n- isolation: none") (#2036)
    // NERF agents may write preferences files without frontmatter delimiters.
    if (/^##\s+\w/m.test(content)) {
        return parseHeadingListFormat(content);
    }
    if (!_warnedUnrecognizedFormat) {
        _warnedUnrecognizedFormat = true;
        console.warn("[parsePreferencesMarkdown] PREFERENCES.md exists but uses an unrecognized format — skipping.");
    }
    return null;
}
function parseFrontmatterBlock(frontmatter) {
    try {
        const parsed = parseYaml(frontmatter);
        if (typeof parsed !== 'object' || parsed === null) {
            return {};
        }
        return parsed;
    }
    catch (e) {
        console.error("[parseFrontmatterBlock] YAML parse error:", e);
        return {};
    }
}
/**
 * Parse heading+list format into a nested object, then cast to NerfPreferences.
 * Handles markdown like:
 *   ## Git
 *   - isolation: none
 *   - commit_docs: true
 *   ## Models
 *   - planner: sonnet
 */
function parseHeadingListFormat(content) {
    const result = {};
    let currentSection = null;
    for (const rawLine of content.split('\n')) {
        const line = rawLine.replace(/\r$/, '');
        const headingMatch = line.match(/^##\s+(.+)$/);
        if (headingMatch) {
            currentSection = headingMatch[1].trim().toLowerCase().replace(/\s+/g, '_');
            if (!result[currentSection])
                result[currentSection] = [];
            continue;
        }
        if (currentSection && line.trim() && !line.trimStart().startsWith('#')) {
            result[currentSection].push(line);
        }
    }
    const typed = {};
    for (const [section, lines] of Object.entries(result)) {
        if (lines.length === 0)
            continue;
        const usesLegacyListItems = lines.every((line) => /^\s*-\s+[^:]+:\s*.*$/.test(line));
        const yamlBlock = usesLegacyListItems
            ? lines.map((line) => line.replace(/^\s*-\s+/, '')).join('\n')
            : lines.join('\n');
        try {
            const parsed = parseYaml(yamlBlock);
            if (typeof parsed !== 'object' || parsed === null)
                continue;
            let targetSection = section;
            let value = parsed;
            if (!Array.isArray(parsed)) {
                const keys = Object.keys(parsed);
                if (keys.length === 1) {
                    const [onlyKey] = keys;
                    if (onlyKey === section || (!KNOWN_PREFERENCE_KEYS.has(section) && KNOWN_PREFERENCE_KEYS.has(onlyKey))) {
                        targetSection = onlyKey;
                        value = parsed[onlyKey];
                    }
                }
            }
            typed[targetSection] = value;
        }
        catch {
            /* malformed section — skip */
        }
    }
    return typed;
}
// ─── Merging ────────────────────────────────────────────────────────────────
/**
 * Apply mode defaults as the lowest-priority layer.
 * Mode defaults fill in undefined fields; any explicit user value wins.
 */
export function applyModeDefaults(mode, prefs) {
    const defaults = MODE_DEFAULTS[mode];
    if (!defaults)
        return prefs;
    return mergePreferences(defaults, prefs);
}
function mergePreferences(base, override) {
    return {
        version: override.version ?? base.version,
        mode: override.mode ?? base.mode,
        always_use_skills: mergeStringLists(base.always_use_skills, override.always_use_skills),
        prefer_skills: mergeStringLists(base.prefer_skills, override.prefer_skills),
        avoid_skills: mergeStringLists(base.avoid_skills, override.avoid_skills),
        skill_rules: [...(base.skill_rules ?? []), ...(override.skill_rules ?? [])],
        custom_instructions: mergeStringLists(base.custom_instructions, override.custom_instructions),
        models: { ...(base.models ?? {}), ...(override.models ?? {}) },
        skill_discovery: override.skill_discovery ?? base.skill_discovery,
        skill_staleness_days: override.skill_staleness_days ?? base.skill_staleness_days,
        auto_supervisor: { ...(base.auto_supervisor ?? {}), ...(override.auto_supervisor ?? {}) },
        uat_dispatch: override.uat_dispatch ?? base.uat_dispatch,
        unique_milestone_ids: override.unique_milestone_ids ?? base.unique_milestone_ids,
        budget_ceiling: override.budget_ceiling ?? base.budget_ceiling,
        budget_enforcement: override.budget_enforcement ?? base.budget_enforcement,
        context_pause_threshold: override.context_pause_threshold ?? base.context_pause_threshold,
        notifications: (base.notifications || override.notifications)
            ? { ...(base.notifications ?? {}), ...(override.notifications ?? {}) }
            : undefined,
        cmux: (base.cmux || override.cmux)
            ? { ...(base.cmux ?? {}), ...(override.cmux ?? {}) }
            : undefined,
        remote_questions: override.remote_questions
            ? { ...(base.remote_questions ?? {}), ...override.remote_questions }
            : base.remote_questions,
        git: (base.git || override.git)
            ? { ...(base.git ?? {}), ...(override.git ?? {}) }
            : undefined,
        post_unit_hooks: mergePostUnitHooks(base.post_unit_hooks, override.post_unit_hooks),
        pre_dispatch_hooks: mergePreDispatchHooks(base.pre_dispatch_hooks, override.pre_dispatch_hooks),
        dynamic_routing: (base.dynamic_routing || override.dynamic_routing)
            ? { ...(base.dynamic_routing ?? {}), ...(override.dynamic_routing ?? {}) }
            : undefined,
        token_profile: override.token_profile ?? base.token_profile,
        phases: (base.phases || override.phases)
            ? { ...(base.phases ?? {}), ...(override.phases ?? {}) }
            : undefined,
        parallel: (base.parallel || override.parallel)
            ? { ...(base.parallel ?? {}), ...(override.parallel ?? {}) }
            : undefined,
        verification_commands: mergeStringLists(base.verification_commands, override.verification_commands),
        verification_auto_fix: override.verification_auto_fix ?? base.verification_auto_fix,
        verification_max_retries: override.verification_max_retries ?? base.verification_max_retries,
        search_provider: override.search_provider ?? base.search_provider,
        context_selection: override.context_selection ?? base.context_selection,
        auto_visualize: override.auto_visualize ?? base.auto_visualize,
        auto_report: override.auto_report ?? base.auto_report,
        github: (base.github || override.github)
            ? { ...(base.github ?? {}), ...(override.github ?? {}) }
            : undefined,
        service_tier: override.service_tier ?? base.service_tier,
        forensics_dedup: override.forensics_dedup ?? base.forensics_dedup,
        show_token_cost: override.show_token_cost ?? base.show_token_cost,
        experimental: (base.experimental || override.experimental)
            ? { ...(base.experimental ?? {}), ...(override.experimental ?? {}) }
            : undefined,
    };
}
function mergeStringLists(base, override) {
    const merged = [
        ...normalizeStringArray(base),
        ...normalizeStringArray(override),
    ]
        .map((item) => item.trim())
        .filter(Boolean);
    return merged.length > 0 ? Array.from(new Set(merged)) : undefined;
}
function mergePostUnitHooks(base, override) {
    if (!base?.length && !override?.length)
        return undefined;
    const merged = [...(base ?? [])];
    for (const hook of override ?? []) {
        // Override hooks with same name replace base hooks
        const idx = merged.findIndex(h => h.name === hook.name);
        if (idx >= 0) {
            merged[idx] = hook;
        }
        else {
            merged.push(hook);
        }
    }
    return merged.length > 0 ? merged : undefined;
}
function mergePreDispatchHooks(base, override) {
    if (!base?.length && !override?.length)
        return undefined;
    const merged = [...(base ?? [])];
    for (const hook of override ?? []) {
        const idx = merged.findIndex(h => h.name === hook.name);
        if (idx >= 0) {
            merged[idx] = hook;
        }
        else {
            merged.push(hook);
        }
    }
    return merged.length > 0 ? merged : undefined;
}
// ─── System Prompt Rendering ──────────────────────────────────────────────────
export function renderPreferencesForSystemPrompt(preferences, resolutions) {
    const validated = validatePreferences(preferences);
    const lines = ["## NERF Skill Preferences"];
    if (validated.errors.length > 0) {
        lines.push("- Validation: some preference values were ignored because they were invalid.");
    }
    for (const warning of validated.warnings) {
        lines.push(`- Deprecation: ${warning}`);
    }
    preferences = validated.preferences;
    lines.push("- Treat these as explicit skill-selection policy for NERF work.", "- If a listed skill exists and is relevant, load and follow it instead of treating it as a vague suggestion.", "- Current user instructions still override these defaults.");
    const fmt = (ref) => resolutions ? formatSkillRef(ref, resolutions) : ref;
    if (preferences.always_use_skills && preferences.always_use_skills.length > 0) {
        lines.push("- Always use these skills when relevant:");
        for (const skill of preferences.always_use_skills) {
            lines.push(`  - ${fmt(skill)}`);
        }
    }
    if (preferences.prefer_skills && preferences.prefer_skills.length > 0) {
        lines.push("- Prefer these skills when relevant:");
        for (const skill of preferences.prefer_skills) {
            lines.push(`  - ${fmt(skill)}`);
        }
    }
    if (preferences.avoid_skills && preferences.avoid_skills.length > 0) {
        lines.push("- Avoid these skills unless clearly needed:");
        for (const skill of preferences.avoid_skills) {
            lines.push(`  - ${fmt(skill)}`);
        }
    }
    if (preferences.skill_rules && preferences.skill_rules.length > 0) {
        lines.push("- Situational rules:");
        for (const rule of preferences.skill_rules) {
            lines.push(`  - When ${rule.when}:`);
            if (rule.use && rule.use.length > 0) {
                lines.push(`    - use: ${rule.use.map(fmt).join(", ")}`);
            }
            if (rule.prefer && rule.prefer.length > 0) {
                lines.push(`    - prefer: ${rule.prefer.map(fmt).join(", ")}`);
            }
            if (rule.avoid && rule.avoid.length > 0) {
                lines.push(`    - avoid: ${rule.avoid.map(fmt).join(", ")}`);
            }
        }
    }
    if (preferences.custom_instructions && preferences.custom_instructions.length > 0) {
        lines.push("- Additional instructions:");
        for (const instruction of preferences.custom_instructions) {
            lines.push(`  - ${instruction}`);
        }
    }
    return lines.join("\n");
}
// ─── Hook Resolution ──────────────────────────────────────────────────────────
/**
 * Resolve enabled post-unit hooks from effective preferences.
 * Returns an empty array when no hooks are configured.
 */
export function resolvePostUnitHooks() {
    const prefs = loadEffectiveNerfPreferences();
    return (prefs?.preferences.post_unit_hooks ?? [])
        .filter(h => h.enabled !== false);
}
/**
 * Resolve enabled pre-dispatch hooks from effective preferences.
 * Returns an empty array when no hooks are configured.
 */
export function resolvePreDispatchHooks() {
    const prefs = loadEffectiveNerfPreferences();
    return (prefs?.preferences.pre_dispatch_hooks ?? [])
        .filter(h => h.enabled !== false);
}
// ─── Isolation & Parallel ─────────────────────────────────────────────────────
/**
 * Resolve the effective git isolation mode from preferences.
 * Returns "none" (default), "worktree", or "branch".
 *
 * Default is "none" so NERF works out of the box without PREFERENCES.md.
 * Worktree isolation requires explicit opt-in because it depends on git
 * branch infrastructure that must be set up before use.
 */
export function getIsolationMode() {
    const prefs = loadEffectiveNerfPreferences()?.preferences?.git;
    if (prefs?.isolation === "worktree")
        return "worktree";
    if (prefs?.isolation === "branch")
        return "branch";
    return "none"; // default — no isolation, work on current branch
}
export function resolveParallelConfig(prefs) {
    return {
        enabled: prefs?.parallel?.enabled ?? false,
        max_workers: Math.max(1, Math.min(4, prefs?.parallel?.max_workers ?? 2)),
        budget_ceiling: prefs?.parallel?.budget_ceiling,
        merge_strategy: prefs?.parallel?.merge_strategy ?? "per-milestone",
        auto_merge: prefs?.parallel?.auto_merge ?? "confirm",
    };
}
