import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { bobbitConfigDir } from "../bobbit-dir.js";
import { removeMount as removePreviewMount } from "../preview/mount.js";
import { getAllConfigDirectories } from "./config-directories.js";
import { profile, bumpCount } from "./profiling.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
/**
 * Resolve the active system-prompt.md path.
 * Prefers the user override at `<bobbitConfigDir()>/system-prompt.md`;
 * falls back to the shipped `<defaultsDir>/system-prompt.md`.
 * Returns `undefined` only when neither file exists.
 */
export function resolveSystemPromptPath() {
    const userPath = path.join(bobbitConfigDir(), "system-prompt.md");
    if (fs.existsSync(userPath))
        return userPath;
    // __dirname here is dist/server/agent/, so defaults live at dist/server/defaults/.
    const defaultPath = path.join(__dirname, "..", "defaults", "system-prompt.md");
    if (fs.existsSync(defaultPath))
        return defaultPath;
    return undefined;
}
/** Module-level cache of the prompts directory. Set once by ensurePromptsDir(). */
let _promptsDir;
/** Initialize the prompts directory from a stateDir. Called by server startup. */
export function initPromptDirs(stateDir) {
    _promptsDir = path.join(stateDir, "session-prompts");
    if (!fs.existsSync(_promptsDir)) {
        fs.mkdirSync(_promptsDir, { recursive: true });
    }
}
function getPromptsDir() {
    if (!_promptsDir)
        throw new Error("system-prompt: initPromptDirs() not called");
    // Defensive recreate: the dir is created once at startup but may be removed
    // mid-run by external cleanup (test teardown, maintenance, AV quirks).
    // Recreating on access keeps writes robust without masking real errors.
    if (!fs.existsSync(_promptsDir))
        fs.mkdirSync(_promptsDir, { recursive: true });
    return _promptsDir;
}
/**
 * Resolve `@path` references in markdown content, matching Claude Code behavior.
 *
 * `@path/to/file.ext` references are expanded inline wherever they appear on a
 * line. Both relative and absolute paths are supported; relative paths resolve
 * from `baseDir`. References are resolved recursively (a referenced file can
 * itself contain `@` references) up to a maximum depth of 5 hops. A `seen` set
 * prevents infinite loops. Paths starting with `~` expand to the home directory.
 *
 * When a `@ref` is the **only** content on a line (with optional leading
 * whitespace), the file content replaces the entire line and inherits the
 * leading indentation. When inline with other text, the file content is
 * inserted in place of the `@ref` token (no indentation adjustment).
 */
export function resolveMarkdownRefs(content, baseDir, seen, depth = 0) {
    if (!seen)
        seen = new Set();
    const MAX_DEPTH = 5;
    // First pass: whole-line refs (preserves indentation behavior)
    content = content.replace(/^([ \t]*)@(\S+)\s*$/gm, (_match, indent, refPath) => {
        return resolveOneRef(refPath, indent, baseDir, seen, depth, MAX_DEPTH, /* wholeLine */ true);
    });
    // Second pass: inline refs (surrounded by other text)
    // Negative lookbehind excludes email addresses (word char before @)
    content = content.replace(/(?<!\w)@((?:~[/\\]|\.{0,2}[/\\])?[\w./_\\-]+\.\w+)/g, (_match, refPath) => {
        return resolveOneRef(refPath, "", baseDir, seen, depth, MAX_DEPTH, /* wholeLine */ false);
    });
    return content;
}
function expandHomePath(p) {
    if (p.startsWith("~")) {
        return path.join(os.homedir(), p.slice(1));
    }
    return p;
}
function resolveOneRef(refPath, indent, baseDir, seen, depth, maxDepth, wholeLine) {
    const filePath = path.resolve(baseDir, expandHomePath(refPath));
    const canonical = path.normalize(filePath);
    if (seen.has(canonical)) {
        return wholeLine ? `${indent}<!-- circular reference: ${refPath} -->` : `<!-- circular reference: ${refPath} -->`;
    }
    if (!fs.existsSync(filePath)) {
        return wholeLine ? `${indent}<!-- file not found: ${refPath} -->` : `<!-- file not found: ${refPath} -->`;
    }
    if (depth >= maxDepth) {
        return wholeLine ? `${indent}<!-- max import depth reached: ${refPath} -->` : `<!-- max import depth reached: ${refPath} -->`;
    }
    seen.add(canonical);
    try {
        const refContent = fs.readFileSync(filePath, "utf-8");
        const resolved = resolveMarkdownRefs(refContent, path.dirname(filePath), seen, depth + 1);
        if (wholeLine && indent) {
            return resolved
                .split("\n")
                .map((line) => (line.trim() ? indent + line : line))
                .join("\n");
        }
        return resolved;
    }
    catch {
        return wholeLine ? `${indent}<!-- error reading: ${refPath} -->` : `<!-- error reading: ${refPath} -->`;
    }
}
/**
 * Read an AGENTS.md file from a directory, resolving `@` references.
 * Returns the resolved content, or empty string if no file exists.
 * Looks for AGENTS.md (case-sensitive).
 */
export function readAgentsMd(cwd) {
    const agentsPath = path.join(cwd, "AGENTS.md");
    if (!fs.existsSync(agentsPath))
        return "";
    try {
        const raw = fs.readFileSync(agentsPath, "utf-8");
        return resolveMarkdownRefs(raw, cwd);
    }
    catch {
        return "";
    }
}
/**
 * Read all agent markdown files from configured locations.
 * Collects entries with type "agents" from getAllConfigDirectories(),
 * reads each existing file, resolves @refs, and concatenates.
 * Falls back to readAgentsMd() if no projectConfigStore is provided.
 */
export function readAllAgentFiles(cwd, projectConfigStore) {
    return profile("readAllAgentFiles", () => {
        if (!projectConfigStore) {
            return readAgentsMd(cwd);
        }
        const dirs = getAllConfigDirectories(cwd, projectConfigStore);
        const agentEntries = dirs.filter(d => d.types.includes("agents") && d.exists);
        bumpCount("readAllAgentFiles.files", agentEntries.length);
        const parts = [];
        for (const entry of agentEntries) {
            try {
                const content = fs.readFileSync(entry.path, "utf-8");
                const resolved = resolveMarkdownRefs(content, path.dirname(entry.path));
                if (resolved.trim()) {
                    parts.push(resolved.trim());
                }
            }
            catch (err) {
                console.warn(`[system-prompt] Failed to read agent file ${entry.path}, skipping:`, err);
            }
        }
        return parts.join("\n\n");
    });
}
/** Default max bytes of skills-catalog markdown to embed in the system prompt. */
export const SKILLS_CATALOG_BUDGET = 16384;
/** Lower bound for a user-configured skills-catalog byte budget. */
export const SKILLS_CATALOG_BUDGET_MIN = 1024;
/** Upper bound for a user-configured skills-catalog byte budget. */
export const SKILLS_CATALOG_BUDGET_MAX = 131072;
/**
 * Resolve a possibly-undefined override into the effective skills-catalog budget.
 * - `undefined`, `NaN`, or non-finite → `SKILLS_CATALOG_BUDGET`.
 * - Otherwise clamps `Math.floor(override)` to `[MIN, MAX]`.
 */
export function resolveSkillsCatalogBudget(override) {
    if (override === undefined || override === null)
        return SKILLS_CATALOG_BUDGET;
    if (typeof override !== "number" || !Number.isFinite(override))
        return SKILLS_CATALOG_BUDGET;
    const floored = Math.floor(override);
    if (floored < SKILLS_CATALOG_BUDGET_MIN)
        return SKILLS_CATALOG_BUDGET_MIN;
    if (floored > SKILLS_CATALOG_BUDGET_MAX)
        return SKILLS_CATALOG_BUDGET_MAX;
    return floored;
}
/**
 * Build the "Available Skills" section. Skills are listed alphabetically;
 * if the budget is exceeded, the tail is truncated with an `(N more …
 * omitted, alphabetically truncated)` footer.
 *
 * Caller is responsible for filtering out skills with
 * `disable-model-invocation: true` — this function trusts its input.
 */
export function buildSkillsCatalogSection(skills, budgetOverride) {
    if (!skills || skills.length === 0)
        return undefined;
    const budget = resolveSkillsCatalogBudget(budgetOverride);
    const sorted = [...skills].sort((a, b) => a.name.localeCompare(b.name));
    const header = "## Available Skills\n\n" +
        "You can autonomously activate any of the following skills mid-turn by calling " +
        "the `activate_skill` tool with `{ name, args? }`. The tool returns the skill's " +
        "instructions as the tool result; follow them as if the user had typed `/<name> <args>`.\n\n";
    const lines = [];
    let length = header.length;
    let truncated = 0;
    for (let i = 0; i < sorted.length; i++) {
        const s = sorted[i];
        const desc = (s.description || "").replace(/\s+/g, " ").trim();
        const hint = s.argumentHint ? ` _args: ${s.argumentHint}_` : "";
        const line = `- **${s.name}** — ${desc}${hint}`;
        if (length + line.length + 1 > budget) {
            truncated = sorted.length - i;
            break;
        }
        lines.push(line);
        length += line.length + 1;
    }
    if (truncated > 0) {
        lines.push(`- _… (${truncated} more skill${truncated === 1 ? "" : "s"} omitted, alphabetically truncated)_`);
        console.warn(`[system-prompt] Skills catalog exceeded ${budget}B budget — truncated ${truncated} skill(s).`);
    }
    return header + lines.join("\n");
}
/**
 * Assemble the full system prompt from its parts and write to a temp file.
 *
 * Order (stable prefix first, volatile sections last, for provider prompt-cache reuse):
 *   1. Global system prompt (config/system-prompt.md)
 *   2. AGENTS.md from the session's working directory (with @refs resolved inline)
 *   2.5. Working directory instructions
 *   3. Tool documentation
 *   4. Available Skills catalog
 *   5. Goal spec + role (if session belongs to a goal)
 *   6. Current Task
 *   7. Workflow upstream-gate context
 *
 * Returns the path to the assembled prompt file, or undefined if all parts
 * are empty (in which case no --system-prompt should be passed to the agent).
 */
export function assembleSystemPrompt(sessionId, parts) {
    return profile("assembleSystemPrompt", () => _assembleSystemPrompt(sessionId, parts));
}
function _assembleSystemPrompt(sessionId, parts) {
    const sections = [];
    // 1. Global system prompt (resolve @refs relative to its directory)
    if (parts.baseSystemPromptPath && fs.existsSync(parts.baseSystemPromptPath)) {
        const raw = fs.readFileSync(parts.baseSystemPromptPath, "utf-8").trim();
        const base = raw ? resolveMarkdownRefs(raw, path.dirname(parts.baseSystemPromptPath)) : "";
        if (base)
            sections.push(base);
    }
    // 2. Agent files — use projectRoot (host-accessible) when available; for sandboxed
    // agents cwd is a container-internal path the host can't read.
    const filesRoot = parts.projectRoot || parts.cwd;
    const agentsMd = readAllAgentFiles(filesRoot, parts.projectConfigStore);
    if (agentsMd.trim()) {
        sections.push("# Project AGENTS.md\n\n" + agentsMd.trim());
    }
    // 2.5. Working directory instructions
    if (parts.cwd) {
        sections.push(`# Working Directory\n\n` +
            `Your working directory is: \`${parts.cwd}\`\n\n` +
            `Stay in this directory for all file operations and git commands. ` +
            `Do not \`cd\` into other directories unless explicitly required by the task.\n\n` +
            `**Why this is a hard constraint:** Other agents, the dev server, and the user may all be working in the primary worktree simultaneously. ` +
            `If you \`cd\` there and make changes, you risk merge conflicts during rebase, corrupting other agents' in-progress work, or breaking the running dev server. ` +
            `Even for infrastructure files (Dockerfiles, configs), the correct flow is: edit here → commit → push to origin → pull from primary. ` +
            `One \`cd\` violation cascades — all subsequent commands (edit, git add, commit) will operate on shared state.`);
    }
    // 3. Tool documentation (stable across turns — kept in the prefix for prompt caching)
    if (parts.toolDocs?.trim()) {
        sections.push(parts.toolDocs.trim());
    }
    // 4. Available Skills (autonomous activation catalog — stable across turns)
    if (parts.skillsCatalog && parts.skillsCatalog.length > 0) {
        const skillsSection = buildSkillsCatalogSection(parts.skillsCatalog, parts.skillsCatalogBudget);
        if (skillsSection)
            sections.push(skillsSection);
    }
    // 5. Goal spec (merge rolePrompt into goalSpec section for backward compat).
    // Volatile sections (goal/task/workflow context) follow the stable prefix above
    // so provider prompt caches reuse the tool docs + skills catalog between turns.
    {
        let effectiveGoalSpec = parts.goalSpec || "";
        if (parts.rolePrompt?.trim()) {
            effectiveGoalSpec = (effectiveGoalSpec ? effectiveGoalSpec + "\n\n---\n\n" : "") + parts.rolePrompt.trim();
        }
        if (effectiveGoalSpec.trim()) {
            const header = parts.goalTitle
                ? `# Goal\n\n**${parts.goalTitle}** (Status: ${parts.goalState || "unknown"})`
                : "# Goal";
            sections.push(header + "\n\n" + effectiveGoalSpec.trim());
        }
    }
    // 6. Task context
    if (parts.taskTitle || parts.taskType) {
        const taskLines = ["# Current Task"];
        if (parts.taskType)
            taskLines.push(`\n**Type**: ${parts.taskType}`);
        if (parts.taskTitle)
            taskLines.push(`**Title**: ${parts.taskTitle}`);
        if (parts.taskSpec?.trim()) {
            taskLines.push(`\n## Task Specification\n${parts.taskSpec.trim()}`);
        }
        if (parts.taskDependsOn && parts.taskDependsOn.length > 0) {
            taskLines.push("\n## Dependencies\nThis task depends on the following completed tasks:");
            for (const dep of parts.taskDependsOn) {
                taskLines.push(`- ${dep}`);
            }
        }
        sections.push(taskLines.join("\n"));
    }
    // 7. Workflow dependency context (accepted upstream gate content)
    if (parts.workflowContext?.trim()) {
        sections.push(parts.workflowContext.trim());
    }
    if (sections.length === 0)
        return undefined;
    const combined = sections.join("\n\n---\n\n") + "\n";
    bumpCount("assembleSystemPrompt.bytes", combined.length);
    const promptPath = path.join(getPromptsDir(), `${sessionId}.md`);
    fs.writeFileSync(promptPath, combined, "utf-8");
    return promptPath;
}
/**
 * Return the system prompt broken into labeled sections for the inspector UI.
 * Takes the same PromptParts as assembleSystemPrompt but returns structured
 * sections instead of writing to disk.
 */
/** Estimate token count from text (~4 chars per token for Claude models) */
function estimateTokens(text) {
    return Math.ceil(text.length / 4);
}
export function getPromptSections(parts) {
    const sections = [];
    // 1. Global system prompt (resolve @refs relative to its directory)
    if (parts.baseSystemPromptPath && fs.existsSync(parts.baseSystemPromptPath)) {
        const raw = fs.readFileSync(parts.baseSystemPromptPath, "utf-8").trim();
        const base = raw ? resolveMarkdownRefs(raw, path.dirname(parts.baseSystemPromptPath)) : "";
        if (base)
            sections.push({ label: "System Prompt", source: parts.baseSystemPromptPath, content: base, tokens: estimateTokens(base) });
    }
    // 2. Agent files (individual sections per file for provenance)
    const viewerRoot = parts.projectRoot || parts.cwd;
    if (parts.projectConfigStore) {
        const dirs = getAllConfigDirectories(viewerRoot, parts.projectConfigStore);
        const agentEntries = dirs.filter(d => d.types.includes("agents") && d.exists);
        for (const entry of agentEntries) {
            try {
                const content = fs.readFileSync(entry.path, "utf-8");
                const resolved = resolveMarkdownRefs(content, path.dirname(entry.path));
                if (resolved.trim()) {
                    sections.push({ label: "Project AGENTS.md", source: entry.path, content: resolved.trim(), tokens: estimateTokens(resolved.trim()) });
                }
            }
            catch {
                // skip unreadable files
            }
        }
    }
    else {
        // Legacy fallback: single AGENTS.md with absolute path
        const agentsPath = path.join(viewerRoot, "AGENTS.md");
        if (fs.existsSync(agentsPath)) {
            const content = readAgentsMd(viewerRoot);
            if (content.trim()) {
                sections.push({ label: "Project AGENTS.md", source: agentsPath, content: content.trim(), tokens: estimateTokens(content.trim()) });
            }
        }
    }
    // 2.5. Working directory (also included in the prompt file via assembleSystemPrompt;
    // the agent CLI may additionally inject its own "Current working directory" based on --cwd)
    if (parts.cwd) {
        const cwdContent = `Your working directory is: \`${parts.cwd}\`\n\n` +
            `Stay in this directory for all file operations and git commands. ` +
            `Do not \`cd\` into other directories unless explicitly required by the task.\n\n` +
            `**Why this is a hard constraint:** Other agents, the dev server, and the user may all be working in the primary worktree simultaneously. ` +
            `If you \`cd\` there and make changes, you risk merge conflicts during rebase, corrupting other agents' in-progress work, or breaking the running dev server. ` +
            `Even for infrastructure files (Dockerfiles, configs), the correct flow is: edit here → commit → push to origin → pull from primary. ` +
            `One \`cd\` violation cascades — all subsequent commands (edit, git add, commit) will operate on shared state.`;
        sections.push({ label: "Working Directory", source: parts.cwd, content: cwdContent, tokens: estimateTokens(cwdContent) });
    }
    // 3. Tool docs (stable prefix — kept ahead of volatile goal/role/task for cache reuse)
    if (parts.toolDocs?.trim()) {
        sections.push({ label: "Tools", source: "Tool documentation", content: parts.toolDocs.trim(), tokens: estimateTokens(parts.toolDocs.trim()) });
    }
    // 4. Available Skills (stable prefix)
    if (parts.skillsCatalog && parts.skillsCatalog.length > 0) {
        const skillsSection = buildSkillsCatalogSection(parts.skillsCatalog, parts.skillsCatalogBudget);
        if (skillsSection) {
            sections.push({ label: "Available Skills", source: "Slash skills catalog", content: skillsSection, tokens: estimateTokens(skillsSection) });
        }
    }
    // 5. Goal spec (separate from role — volatile section follows the stable prefix)
    if (parts.goalSpec?.trim()) {
        const header = parts.goalTitle
            ? `**${parts.goalTitle}** (Status: ${parts.goalState || "unknown"})`
            : "";
        const goalContent = (header ? header + "\n\n" : "") + parts.goalSpec.trim();
        sections.push({ label: "Goal", source: `Goal: ${parts.goalTitle || "Untitled"}`, content: goalContent, tokens: estimateTokens(goalContent) });
    }
    // 6. Role prompt
    if (parts.rolePrompt?.trim()) {
        sections.push({ label: "Role", source: `Role: ${parts.roleName || "unknown"}`, content: parts.rolePrompt.trim(), tokens: estimateTokens(parts.rolePrompt.trim()) });
    }
    // 7. Task context
    if (parts.taskTitle || parts.taskType) {
        const taskLines = [];
        if (parts.taskType)
            taskLines.push(`**Type**: ${parts.taskType}`);
        if (parts.taskTitle)
            taskLines.push(`**Title**: ${parts.taskTitle}`);
        if (parts.taskSpec?.trim())
            taskLines.push(`\n## Task Specification\n${parts.taskSpec.trim()}`);
        if (parts.taskDependsOn?.length) {
            taskLines.push("\n## Dependencies");
            for (const dep of parts.taskDependsOn)
                taskLines.push(`- ${dep}`);
        }
        const taskContent = taskLines.join("\n");
        sections.push({ label: "Task", source: `Task: ${parts.taskTitle || "Untitled"}`, content: taskContent, tokens: estimateTokens(taskContent) });
    }
    // 8. Workflow context
    if (parts.workflowContext?.trim()) {
        sections.push({ label: "Workflow Context", source: "Upstream gates", content: parts.workflowContext.trim(), tokens: estimateTokens(parts.workflowContext.trim()) });
    }
    return sections;
}
/**
 * Persist the resolved prompt sections as a JSON snapshot at session creation time.
 * This captures the actual prompt that was used, not a reconstruction from current files.
 */
export function persistPromptSections(sessionId, parts) {
    try {
        const sections = getPromptSections(parts);
        const totalTokens = sections.reduce((sum, s) => sum + s.tokens, 0);
        const data = { sections, totalTokens, createdAt: new Date().toISOString() };
        const jsonPath = path.join(getPromptsDir(), `${sessionId}-prompt.json`);
        fs.writeFileSync(jsonPath, JSON.stringify(data), "utf-8");
    }
    catch (err) {
        console.error(`[system-prompt] Failed to persist prompt sections for ${sessionId}:`, err);
    }
}
/**
 * Load persisted prompt sections snapshot for a session.
 * Returns null if the file doesn't exist or can't be parsed.
 */
export function loadPersistedPromptSections(sessionId) {
    try {
        const jsonPath = path.join(getPromptsDir(), `${sessionId}-prompt.json`);
        if (!fs.existsSync(jsonPath))
            return null;
        return JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
    }
    catch {
        return null;
    }
}
/**
 * Delete the persisted prompt sections JSON for a session (archive purge only).
 */
export function purgePromptSectionsJson(sessionId) {
    try {
        const jsonPath = path.join(getPromptsDir(), `${sessionId}-prompt.json`);
        if (fs.existsSync(jsonPath))
            fs.unlinkSync(jsonPath);
    }
    catch { /* ignore */ }
}
/**
 * Clean up a session's assembled prompt file.
 */
export function cleanupSessionPrompt(sessionId) {
    const promptPath = path.join(getPromptsDir(), `${sessionId}.md`);
    try {
        if (fs.existsSync(promptPath))
            fs.unlinkSync(promptPath);
    }
    catch { /* ignore */ }
    // Per-session preview mount (WP-A): <stateDir>/preview/<sid>/.
    try {
        removePreviewMount(sessionId);
    }
    catch { /* ignore */ }
}
//# sourceMappingURL=system-prompt.js.map