/**
 * SF 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, dirname, resolve } from "node:path";

import { sfRoot } from "./paths.js";
import { parse as parseYaml } from "yaml";
import type { PostUnitHookConfig, PreDispatchHookConfig, TokenProfile } from "./types.js";
import type { DynamicRoutingConfig } from "./model-router.js";
import { normalizeStringArray } from "../shared/format-utils.js";
import { logWarning } from "./workflow-logger.js";
import { resolveProfileDefaults as _resolveProfileDefaults } from "./preferences-models.js";

import {
  KNOWN_PREFERENCE_KEYS,
  MODE_DEFAULTS,
  type WorkflowMode,
  type SFPreferences,
  type LoadedSFPreferences,
  type SkillResolution,
  type SkillDiscoveryMode,
  formatSkillRef,
} from "./preferences-types.js";
import { validatePreferences } from "./preferences-validation.js";

// ─── Re-exports: types ──────────────────────────────────────────────────────
// Every type/interface that was previously exported from this file is
// re-exported so that downstream `import { Foo } from "./preferences.js"`
// statements keep compiling.

export type {
  WorkflowMode,
  SFSkillRule,
  SFPhaseModelConfig,
  SFModelConfig,
  SFModelConfigV2,
  ResolvedModelConfig,
  SkillDiscoveryMode,
  AutoSupervisorConfig,
  RemoteQuestionsConfig,
  CmuxPreferences,
  UokTurnActionMode,
  UokPreferences,
  CodebaseMapPreferences,
  SFPreferences,
  LoadedSFPreferences,
  SkillResolution,
  SkillResolutionReport,
} from "./preferences-types.js";

// ─── Re-exports: validation ─────────────────────────────────────────────────
export { validatePreferences } from "./preferences-validation.js";

// ─── Re-exports: skills ─────────────────────────────────────────────────────
export { resolveAllSkillReferences } from "./preferences-skills.js";

// These lived in preferences-skills.ts but imported loadEffectiveSFPreferences
// back from this file, creating a circular dependency. Moved here since they
// are trivial wrappers over loadEffectiveSFPreferences.
export function resolveSkillDiscoveryMode(): SkillDiscoveryMode {
  const prefs = loadEffectiveSFPreferences();
  return prefs?.preferences.skill_discovery ?? "suggest";
}

export function resolveSkillStalenessDays(): number {
  const prefs = loadEffectiveSFPreferences();
  return prefs?.preferences.skill_staleness_days ?? 60;
}

// ─── Re-exports: models ─────────────────────────────────────────────────────
export {
  resolveModelForUnit,
  resolveModelWithFallbacksForUnit,
  resolvePersistModelChanges,
  getNextFallbackModel,
  isTransientNetworkError,
  validateModelId,
  updatePreferencesModels,
  resolveDynamicRoutingConfig,
  resolveAutoSupervisorConfig,
  resolveProfileDefaults,
  resolveEffectiveProfile,
  resolveInlineLevel,
  resolveContextSelection,
  resolveSearchProviderFromPreferences,
} from "./preferences-models.js";

// ─── Path Constants & Getters ───────────────────────────────────────────────

function sfHome(): string {
  return process.env.SF_HOME || join(homedir(), ".sf");
}

function globalPreferencesPath(): string {
  return join(sfHome(), "preferences.md");
}

function legacyGlobalPreferencesPath(): string {
  return join(homedir(), ".pi", "agent", "sf-preferences.md");
}

/**
 * Resolve the "project root" for preferences. When SF is running inside a
 * git worktree (e.g. `.sf/worktrees/M003/`), project-level prefs should
 * come from the MAIN worktree's `.sf/PREFERENCES.md`, not the milestone
 * branch's frozen copy. Otherwise a pref change on main never reaches an
 * in-flight milestone, and we saw this in practice: updating PREFERENCES
 * on main had no effect until the milestone branch merged main.
 *
 * Strategy: read `.git` in the current worktree. If it's a file of the
 * form `gitdir: <path>`, we're in a linked worktree — resolve the
 * commondir and walk up one level to reach the main worktree. Fall back
 * to cwd silently for non-worktree setups (bare clones, submodules, etc.
 * where the current logic is already correct).
 */
function projectPrefsRoot(): string {
  const cwd = process.cwd();
  try {
    const gitPath = join(cwd, ".git");
    if (!existsSync(gitPath)) return cwd;
    const stat = readFileSync(gitPath, "utf-8");
    // Linked worktrees have `.git` as a FILE: "gitdir: /abs/path/to/main/.git/worktrees/NAME"
    const m = /^gitdir:\s*(.+)$/m.exec(stat.trim());
    if (!m) return cwd;
    const gitdir = m[1].trim();
    // gitdir looks like /.../.git/worktrees/NAME — commondir is the /.../.git/ part;
    // the main worktree is the directory containing that .git dir.
    // Use the `commondir` pointer file inside the linked worktree's gitdir when present.
    const commondirFile = join(gitdir, "commondir");
    if (existsSync(commondirFile)) {
      const rel = readFileSync(commondirFile, "utf-8").trim();
      const commondir = resolve(gitdir, rel);  // usually "../.." → /.../.git
      const mainRoot = dirname(commondir);      // /.../ (main worktree root)
      if (existsSync(join(mainRoot, ".sf"))) return mainRoot;
    }
  } catch { /* non-fatal — fall back to cwd */ }
  return cwd;
}

function projectPreferencesPath(): string {
  return join(sfRoot(projectPrefsRoot()), "preferences.md");
}
// Bootstrap in gitignore.ts historically created PREFERENCES.md (uppercase) by mistake.
// Check uppercase as a fallback so those files aren't silently ignored.
function globalPreferencesPathUppercase(): string {
  return join(sfHome(), "PREFERENCES.md");
}
function projectPreferencesPathUppercase(): string {
  return join(sfRoot(projectPrefsRoot()), "PREFERENCES.md");
}

export function getGlobalSFPreferencesPath(): string {
  return globalPreferencesPath();
}

export function getLegacyGlobalSFPreferencesPath(): string {
  return legacyGlobalPreferencesPath();
}

export function getProjectSFPreferencesPath(): string {
  return projectPreferencesPath();
}

// ─── Loading ────────────────────────────────────────────────────────────────

export function loadGlobalSFPreferences(): LoadedSFPreferences | null {
  return loadPreferencesFile(globalPreferencesPath(), "global")
    ?? loadPreferencesFile(globalPreferencesPathUppercase(), "global")
    ?? loadPreferencesFile(legacyGlobalPreferencesPath(), "global");
}

export function loadProjectSFPreferences(): LoadedSFPreferences | null {
  return loadPreferencesFile(projectPreferencesPath(), "project")
    ?? loadPreferencesFile(projectPreferencesPathUppercase(), "project");
}

export function loadEffectiveSFPreferences(): LoadedSFPreferences | null {
  const globalPreferences = loadGlobalSFPreferences();
  const projectPreferences = loadProjectSFPreferences();

  if (!globalPreferences && !projectPreferences) return null;

  let result: LoadedSFPreferences;
  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 as TokenProfile | undefined;
  if (profile) {
    const profileDefaults = _resolveProfileDefaults(profile);
    result = {
      ...result,
      preferences: mergePreferences(profileDefaults as SFPreferences, 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: string, scope: "global" | "project"): LoadedSFPreferences | null {
  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;
let _warnedSectionParse = false;

/** @internal Reset the warn-once flags — exported for testing only. */
export function _resetParseWarningFlag(): void {
  _warnedUnrecognizedFormat = false;
  _warnedFrontmatterParse = false;
  _warnedSectionParse = false;
}

/** @internal Exported for testing only */
export function parsePreferencesMarkdown(content: string): SFPreferences | null {
  // 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)
  // SF agents may write preferences files without frontmatter delimiters.
  if (/^##\s+\w/m.test(content)) {
    return parseHeadingListFormat(content);
  }

  // Warn when a non-empty file exists but lacks frontmatter delimiters (#2036).
  if (content.trim().length > 0 && !_warnedUnrecognizedFormat) {
    _warnedUnrecognizedFormat = true;
    console.warn(
      "[SF] Warning: preferences file has unrecognized format — content does not use YAML frontmatter delimiters (---). " +
      "Wrap your preferences in --- fences. See https://github.com/singularity-forge/sf-run/issues/2036",
    );
  }
  return null;
}

let _warnedFrontmatterParse = false;
function parseFrontmatterBlock(frontmatter: string): SFPreferences {
  try {
    const parsed = parseYaml(frontmatter);
    if (typeof parsed !== 'object' || parsed === null) {
      return {} as SFPreferences;
    }
    return parsed as SFPreferences;
  } catch (e) {
    // Warn at most once per session to avoid flooding TUI (#3376)
    if (!_warnedFrontmatterParse) {
      _warnedFrontmatterParse = true;
      logWarning("guided", `YAML parse error in preferences frontmatter (suppressing further): ${(e as Error).message}`);
    }
    return {} as SFPreferences;
  }
}

/**
 * Parse heading+list format into a nested object, then cast to SFPreferences.
 * Handles markdown like:
 *   ## Git
 *   - isolation: none
 *   - commit_docs: true
 *   ## Models
 *   - planner: sonnet
 */
function parseHeadingListFormat(content: string): SFPreferences {
  const result: Record<string, string[]> = {};
  let currentSection: string | null = 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: Record<string, unknown> = {};
  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: unknown = 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 as Record<string, unknown>)[onlyKey];
          }
        }
      }

      typed[targetSection] = value;
    } catch (e) {
      if (!_warnedSectionParse) {
        _warnedSectionParse = true;
        logWarning("guided", `preferences section parse failed: ${(e as Error).message}`);
      }
    }
  }

  return typed as SFPreferences;
}

// ─── Merging ────────────────────────────────────────────────────────────────

/**
 * Apply mode defaults as the lowest-priority layer.
 * Mode defaults fill in undefined fields; any explicit user value wins.
 */
export function applyModeDefaults(mode: WorkflowMode, prefs: SFPreferences): SFPreferences {
  const defaults = MODE_DEFAULTS[mode];
  if (!defaults) return prefs;
  return mergePreferences(defaults, prefs);
}

function mergePreferences(base: SFPreferences, override: SFPreferences): SFPreferences {
  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 ?? {}) },
    persist_model_changes: override.persist_model_changes ?? base.persist_model_changes,
    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 ?? {}) } as DynamicRoutingConfig
      : undefined,
    uok: (base.uok || override.uok)
      ? {
          enabled: override.uok?.enabled ?? base.uok?.enabled,
          legacy_fallback: (base.uok?.legacy_fallback || override.uok?.legacy_fallback)
            ? { ...(base.uok?.legacy_fallback ?? {}), ...(override.uok?.legacy_fallback ?? {}) }
            : undefined,
          gates: (base.uok?.gates || override.uok?.gates)
            ? { ...(base.uok?.gates ?? {}), ...(override.uok?.gates ?? {}) }
            : undefined,
          model_policy: (base.uok?.model_policy || override.uok?.model_policy)
            ? { ...(base.uok?.model_policy ?? {}), ...(override.uok?.model_policy ?? {}) }
            : undefined,
          execution_graph: (base.uok?.execution_graph || override.uok?.execution_graph)
            ? { ...(base.uok?.execution_graph ?? {}), ...(override.uok?.execution_graph ?? {}) }
            : undefined,
          gitops: (base.uok?.gitops || override.uok?.gitops)
            ? { ...(base.uok?.gitops ?? {}), ...(override.uok?.gitops ?? {}) }
            : undefined,
          audit_envelope: (
            base.uok?.audit_envelope
            || base.uok?.audit_unified
            || override.uok?.audit_envelope
            || override.uok?.audit_unified
          )
            ? {
                ...(base.uok?.audit_envelope ?? base.uok?.audit_unified ?? {}),
                ...(override.uok?.audit_envelope ?? override.uok?.audit_unified ?? {}),
              }
            : undefined,
          planning_flow: (
            base.uok?.planning_flow
            || base.uok?.plan_v2
            || override.uok?.planning_flow
            || override.uok?.plan_v2
          )
            ? {
                ...(base.uok?.planning_flow ?? base.uok?.plan_v2 ?? {}),
                ...(override.uok?.planning_flow ?? override.uok?.plan_v2 ?? {}),
              }
            : undefined,
        }
      : 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 ?? {}) } as import("./types.js").ParallelConfig
      : 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,
    enhanced_verification: override.enhanced_verification ?? base.enhanced_verification,
    enhanced_verification_pre: override.enhanced_verification_pre ?? base.enhanced_verification_pre,
    enhanced_verification_post: override.enhanced_verification_post ?? base.enhanced_verification_post,
    enhanced_verification_strict: override.enhanced_verification_strict ?? base.enhanced_verification_strict,
    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 ?? {}) } as import("../github-sync/types.js").GitHubSyncConfig
      : undefined,
    experimental: (base.experimental || override.experimental)
      ? { ...(base.experimental ?? {}), ...(override.experimental ?? {}) }
      : 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,
    codebase: (base.codebase || override.codebase)
      ? {
          ...(base.codebase ?? {}),
          ...(override.codebase ?? {}),
          // Merge exclude_patterns arrays rather than overriding
          exclude_patterns: [
            ...((base.codebase?.exclude_patterns) ?? []),
            ...((override.codebase?.exclude_patterns) ?? []),
          ].filter(Boolean),
        }
      : undefined,
    slice_parallel: (base.slice_parallel || override.slice_parallel)
      ? { ...(base.slice_parallel ?? {}), ...(override.slice_parallel ?? {}) }
      : undefined,
    // Fields previously validated but silently dropped here — same class
    // of latent bug as service_tier (fixed separately). Each gets a simple
    // override-wins merge so the preference actually reaches consumers.
    allowed_providers: mergeStringLists(base.allowed_providers, override.allowed_providers),
    provider_preference: override.provider_preference ?? base.provider_preference,
    flat_rate_providers: mergeStringLists(base.flat_rate_providers, override.flat_rate_providers),
    stale_commit_threshold_minutes: override.stale_commit_threshold_minutes ?? base.stale_commit_threshold_minutes,
    widget_mode: override.widget_mode ?? base.widget_mode,
    modelOverrides: (base.modelOverrides || override.modelOverrides)
      ? { ...(base.modelOverrides ?? {}), ...(override.modelOverrides ?? {}) }
      : undefined,
    safety_harness: (base.safety_harness || override.safety_harness)
      ? { ...(base.safety_harness ?? {}), ...(override.safety_harness ?? {}) }
      : undefined,
  };
}

function mergeStringLists(base?: unknown, override?: unknown): string[] | undefined {
  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?: PostUnitHookConfig[],
  override?: PostUnitHookConfig[],
): PostUnitHookConfig[] | undefined {
  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?: PreDispatchHookConfig[],
  override?: PreDispatchHookConfig[],
): PreDispatchHookConfig[] | undefined {
  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: SFPreferences, resolutions?: Map<string, SkillResolution>): string {
  const validated = validatePreferences(preferences);
  const lines: string[] = ["## SF 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 SF 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: string) => 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(): PostUnitHookConfig[] {
  const prefs = loadEffectiveSFPreferences();
  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(): PreDispatchHookConfig[] {
  const prefs = loadEffectiveSFPreferences();
  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 SF 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(): "none" | "worktree" | "branch" {
  const prefs = loadEffectiveSFPreferences()?.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: SFPreferences | undefined): import("./types.js").ParallelConfig {
  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",
    worker_model: prefs?.parallel?.worker_model,
    worker_timeout_minutes: prefs?.parallel?.worker_timeout_minutes,
  };
}
