/*
 * Copyright 2026, Jaime Terrats.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { readFileSync, existsSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
import { wrapWithBoundary } from '../../services/prompt-sanitizer.js';
import { preprocessContext, getDefaultConfig } from '../../services/context-preprocessor.js';
// ─── Constants ─────────────────────────────────────────────────────────────
const DEFAULT_MAX_BYTES = 24 * 1024;
const TOOLING_CONTRACT = `
## Tooling Contract (MANDATORY)

You are operating inside the setup-agents web console. All workspace operations MUST go through the \`sf setup-agents\` CLI.

### Primary Commands
- \`sf setup-agents init\` — initialize or reconfigure a workspace
- \`sf setup-agents serve\` — start the bridge server
- \`sf setup-agents dashboard\` — generate insights dashboard
- \`sf setup-agents insights\` — run analysis passes
- \`sf setup-agents diagram migrate\` — regenerate diagrams
- \`sf setup-agents canvas sync\` — sync canvas files
- \`sf setup-agents docs sync\` — sync documentation

### Workflow Commands
- \`sf setup-agents task create\` — register new work items
- \`sf setup-agents task list\` — list existing tasks
- \`sf setup-agents workflow execute\` — execute orchestrated workflows
- \`sf setup-agents evidence add\` — record delivery evidence
- \`sf setup-agents review request\` — request reviews

### Mode Behavior
- **Ask mode**: Answer questions using only file reads and searches. Do NOT suggest running commands or making changes — only explain, analyze, and answer.
- **Plan mode**: Analyze the request, research the codebase, then create a task with \`sf setup-agents task create\` containing the implementation plan (title, goal, acceptance criteria, files to modify). Do NOT edit files — output a plan the user can approve before switching to Agent mode.
  - When you need user input to refine the plan, use structured prompts:
    [prompt:radio|Question|Option 1,Option 2,Option 3]
    [prompt:checkbox|Select all that apply|Option A,Option B,Option C]
    [prompt:text|Any additional context?]
    The user will see interactive form elements and submit their choices.
- **Agent mode**: Execute the plan. Create the task if not already created, implement changes, record evidence with \`sf setup-agents evidence add\`, and commit with git.

### Rules
- ALWAYS use \`sf setup-agents <command>\` for workspace configuration, profile setup, rule generation, task tracking, and diagnostics.
- NEVER write to .cursor/rules/, .claude/, .codex/, or tool config directories directly — use \`sf setup-agents init\` to regenerate.
- NEVER edit .agent-workflow/ state files manually — use \`sf setup-agents\` workflow commands.
- You may use \`git\` for version control operations (status, diff, log, add, commit).
- If a task requires code changes, create the task first with \`sf setup-agents task create\`, implement the change, then record evidence.
- When asked to generate diagrams, ERDs, or documentation artifacts, use \`sf setup-agents diagram\` or \`sf setup-agents docs\`.

### Output Format
- Use markdown formatting: headers, bullet lists, code blocks, and tables for structured data.
- Keep responses concise — lead with the answer or action, then details.
- When showing commands you ran or will run, use \`\`\`bash code blocks.
- When presenting a plan or task, use this structure:
  **Task:** <title>
  **Goal:** <one-liner>
  **Files:** <comma-separated paths>
  **Acceptance Criteria:**
  - [ ] criterion 1
  - [ ] criterion 2
- When reporting results, include: what changed, what was verified, and what's next.
- For errors or blockers, state the problem first, then the proposed solution.
- Never dump raw CLI output without explanation — summarize findings in human-readable form.
`;
// ─── Public API ────────────────────────────────────────────────────────────
export function buildContext(workspace, message, options) {
    const maxBytes = options?.maxBytes ?? DEFAULT_MAX_BYTES;
    const sections = [];
    const matchedSkills = [];
    // Budget allocations (bytes)
    // P0 Tooling contract: ~1KB (always included)
    // P1 Project header: ~0.2KB (always included)
    const P2_BUDGET = 8 * 1024; // Matched skills
    const P3_BUDGET = 4 * 1024; // Vault context
    // ─── P0: Tooling contract (always included) ────────────────────────────
    sections.push(wrapWithBoundary(TOOLING_CONTRACT.trim(), { source: 'tooling-contract', trust: 'system' }));
    // ─── P1: Project header ────────────────────────────────────────────────
    const projectHeader = buildProjectHeader(workspace);
    sections.push(wrapWithBoundary(projectHeader, { source: 'header', trust: 'operator' }));
    // ─── P2: Matched skills ────────────────────────────────────────────────
    if (message.length > 0) {
        const skills = matchSkills(workspace, message);
        const topSkills = skills.slice(0, 3);
        let skillBudgetRemaining = P2_BUDGET;
        for (const skill of topSkills) {
            const processed = fitContentToBudget(skill.content, skillBudgetRemaining);
            if (processed.length > 0) {
                sections.push(wrapWithBoundary(processed, { source: `skill:${skill.id}`, trust: 'operator' }));
                matchedSkills.push(skill.id);
                skillBudgetRemaining -= Buffer.byteLength(processed, 'utf8');
                if (skillBudgetRemaining <= 0)
                    break;
            }
        }
    }
    // ─── P3: Vault context ─────────────────────────────────────────────────
    if (options?.vaultFiles && options.vaultFiles.length > 0) {
        const vaultSection = buildVaultSection(options.vaultFiles, message, P3_BUDGET);
        if (vaultSection.length > 0) {
            sections.push(wrapWithBoundary(vaultSection, { source: 'vault', trust: 'document' }));
        }
    }
    // ─── P4: Workspace docs ─────────────────────────────────────────────────
    // NOTE: CLAUDE.md/AGENTS.md are NOT injected here because the Claude CLI
    // automatically reads them from cwd. Injecting them would cause double-loading
    // and inflate the prompt beyond the model's context window.
    // ─── P5: Skill index (remaining available skills as one-liners) ────────
    const skillIndex = buildSkillIndex(workspace, matchedSkills);
    if (skillIndex.length > 0) {
        sections.push(wrapWithBoundary(skillIndex, { source: 'skill-index', trust: 'operator' }));
    }
    // ─── Assemble and enforce final budget ─────────────────────────────────
    let context = sections.join('\n');
    if (Buffer.byteLength(context, 'utf8') > maxBytes) {
        // Truncate to fit within budget (preserving valid UTF-8 boundary)
        const buf = Buffer.from(context, 'utf8');
        context = buf.subarray(0, maxBytes).toString('utf8');
        // Trim to last complete line to avoid partial tags
        const lastNewline = context.lastIndexOf('\n');
        if (lastNewline > maxBytes * 0.8) {
            context = context.slice(0, lastNewline) + '\n[...truncated]';
        }
    }
    return { context, matchedSkills };
}
export function matchSkills(workspace, message) {
    const skillsDir = join(workspace, '.setup-agents', 'skills');
    if (!existsSync(skillsDir))
        return [];
    let skillDirs;
    try {
        skillDirs = readdirSync(skillsDir, { withFileTypes: true })
            .filter((d) => d.isDirectory())
            .map((d) => d.name);
    }
    catch {
        return [];
    }
    const messageWords = new Set(message
        .toLowerCase()
        .split(/[\s,.:;!?()[\]{}"'`]+/)
        .filter((w) => w.length > 2));
    const results = [];
    for (const dir of skillDirs) {
        const skillPath = join(skillsDir, dir, 'SKILL.md');
        if (!existsSync(skillPath))
            continue;
        try {
            const raw = readFileSync(skillPath, 'utf8');
            const { activation, content } = parseSkillFrontmatter(raw);
            if (activation.length === 0)
                continue;
            let score = 0;
            for (const keyword of activation) {
                const normalizedKeyword = keyword.toLowerCase().trim();
                if (normalizedKeyword.length === 0)
                    continue;
                // Also create a collapsed version (no dots/hyphens) for fuzzy matching
                const collapsedKeyword = normalizedKeyword.replace(/[.\-_]/g, '');
                // Check if any message word matches or contains the keyword
                for (const word of messageWords) {
                    const collapsedWord = word.replace(/[.\-_]/g, '');
                    if (word === normalizedKeyword ||
                        word.includes(normalizedKeyword) ||
                        normalizedKeyword.includes(word) ||
                        collapsedWord === collapsedKeyword ||
                        collapsedWord.includes(collapsedKeyword) ||
                        collapsedKeyword.includes(collapsedWord)) {
                        score++;
                        break;
                    }
                }
            }
            if (score >= 1) {
                results.push({ id: dir, content, score });
            }
        }
        catch {
            // Skip unreadable skill files
        }
    }
    // Sort by score descending, return top-3
    results.sort((a, b) => b.score - a.score);
    return results.slice(0, 3);
}
// ─── Internal helpers ──────────────────────────────────────────────────────
function parseSkillFrontmatter(raw) {
    const lines = raw.split('\n');
    if (lines[0] !== '---') {
        // No frontmatter — extract triggers from body "- Trigger:" lines
        const activation = extractBodyTriggers(raw);
        const content = lines.slice(0, 80).join('\n');
        return { activation, content };
    }
    const endIdx = lines.indexOf('---', 1);
    if (endIdx === -1) {
        return { activation: [], content: raw };
    }
    // Parse frontmatter fields
    const frontmatter = lines.slice(1, endIdx);
    let activation = [];
    // Strategy 1: explicit "activation:" field (highest priority)
    for (const line of frontmatter) {
        const match = /^activation:\s*(.+)$/i.exec(line);
        if (match) {
            activation = match[1]
                .split(',')
                .map((k) => k.trim())
                .filter((k) => k.length > 0);
            break;
        }
    }
    // Strategy 2: extract keywords from "USE FOR:" in the description field
    if (activation.length === 0) {
        const descriptionText = extractYamlDescription(frontmatter);
        const useForMatch = /USE FOR:\s*(.+)/i.exec(descriptionText);
        if (useForMatch) {
            activation = useForMatch[1]
                .split(',')
                .map((k) => k.trim().replace(/\.$/, ''))
                .filter((k) => k.length > 0);
        }
    }
    // Strategy 3: extract "- Trigger:" lines from body content
    if (activation.length === 0) {
        const bodyText = lines.slice(endIdx + 1).join('\n');
        activation = extractBodyTriggers(bodyText);
    }
    // Strategy 4: use skill name from frontmatter as last resort
    if (activation.length === 0) {
        for (const line of frontmatter) {
            const nameMatch = /^name:\s*(.+)$/i.exec(line);
            if (nameMatch) {
                // Split hyphenated skill name into words (e.g., "diagram-export" -> ["diagram", "export"])
                activation = nameMatch[1]
                    .trim()
                    .split(/[-_]/)
                    .filter((w) => w.length > 2);
                break;
            }
        }
    }
    // Content is everything after frontmatter (limited to 80 lines)
    const contentLines = lines.slice(endIdx + 1);
    const content = contentLines.slice(0, 80).join('\n');
    return { activation, content };
}
function extractYamlDescription(frontmatterLines) {
    // YAML multi-line scalar: collects "description:" and all continuation lines
    let collecting = false;
    const parts = [];
    for (const line of frontmatterLines) {
        if (/^description:\s*/i.test(line)) {
            collecting = true;
            // Handle single-line description or start of multi-line
            const afterKey = line.replace(/^description:\s*>?-?\s*/i, '');
            if (afterKey.length > 0)
                parts.push(afterKey);
            continue;
        }
        if (collecting) {
            // Continuation lines are indented; stop at next key (non-indented)
            if (/^\S/.test(line))
                break;
            parts.push(line.trim());
        }
    }
    return parts.join(' ');
}
function extractBodyTriggers(text) {
    const triggers = [];
    const triggerRegex = /^-\s*Trigger:\s*`?([^`\n]+)`?\s*$/gim;
    let match;
    while ((match = triggerRegex.exec(text)) !== null) {
        const trigger = match[1].trim();
        if (trigger.length > 0)
            triggers.push(trigger);
    }
    return triggers;
}
function buildProjectHeader(workspace) {
    const projectName = resolveProjectName(workspace);
    const parts = [`Project: ${projectName}`, `Workspace: ${workspace}`];
    const configPath = join(workspace, '.setup-agents', 'config.json');
    if (existsSync(configPath)) {
        try {
            const raw = readFileSync(configPath, 'utf8');
            const config = JSON.parse(raw);
            const profileNames = config.profiles?.map((p) => p.name).join(', ') ?? 'none';
            const tools = config.tools?.join(', ') ?? 'none';
            parts.push(`Profiles: ${profileNames}`);
            parts.push(`Tools: ${tools}`);
        }
        catch {
            // config unreadable, skip
        }
    }
    return parts.join('\n');
}
function buildVaultSection(vaultFiles, message, budget) {
    // Build compact manifest
    const manifest = vaultFiles
        .map((f) => {
        const tags = f.tags && f.tags.length > 0 ? ` [${f.tags.join(', ')}]` : '';
        return `- ${f.path}${tags}`;
    })
        .join('\n');
    // Score vault entries by keyword overlap with message
    const messageWords = new Set(message
        .toLowerCase()
        .split(/[\s,.:;!?()[\]{}"'`]+/)
        .filter((w) => w.length > 2));
    const scored = vaultFiles.map((file) => {
        const fileWords = `${file.path} ${file.tags?.join(' ') ?? ''} ${file.content.slice(0, 500)}`
            .toLowerCase()
            .split(/[\s,.:;!?()[\]{}"'`]+/);
        let score = 0;
        for (const word of fileWords) {
            if (messageWords.has(word))
                score++;
        }
        return { file, score };
    });
    scored.sort((a, b) => b.score - a.score);
    const topEntries = scored.slice(0, 2);
    // Build vault section within budget
    const parts = ['## Context Vault', manifest];
    let used = Buffer.byteLength(parts.join('\n'), 'utf8');
    for (const { file } of topEntries) {
        const entryHeader = `\n### ${file.path}`;
        const entryContent = fitContentToBudget(file.content, budget - used - 100);
        const entry = `${entryHeader}\n${entryContent}`;
        const entryBytes = Buffer.byteLength(entry, 'utf8');
        if (used + entryBytes > budget)
            break;
        parts.push(entry);
        used += entryBytes;
    }
    return parts.join('\n');
}
// buildDocSection removed — Claude CLI auto-reads CLAUDE.md/AGENTS.md from cwd
function buildSkillIndex(workspace, alreadyIncluded) {
    const skillsDir = join(workspace, '.setup-agents', 'skills');
    if (!existsSync(skillsDir))
        return '';
    let skillDirs;
    try {
        skillDirs = readdirSync(skillsDir, { withFileTypes: true })
            .filter((d) => d.isDirectory())
            .map((d) => d.name);
    }
    catch {
        return '';
    }
    const includedSet = new Set(alreadyIncluded);
    const remaining = skillDirs.filter((d) => !includedSet.has(d));
    if (remaining.length === 0)
        return '';
    const lines = ['## Available Skills'];
    for (const dir of remaining) {
        const skillPath = join(skillsDir, dir, 'SKILL.md');
        if (!existsSync(skillPath))
            continue;
        try {
            const raw = readFileSync(skillPath, 'utf8');
            const firstContentLine = getFirstContentLine(raw);
            lines.push(`- **${dir}**: ${firstContentLine}`);
        }
        catch {
            lines.push(`- **${dir}**`);
        }
    }
    return lines.join('\n');
}
function getFirstContentLine(raw) {
    const lines = raw.split('\n');
    let startIdx = 0;
    // Skip frontmatter
    if (lines[0] === '---') {
        const endIdx = lines.indexOf('---', 1);
        if (endIdx !== -1) {
            startIdx = endIdx + 1;
        }
    }
    // Find first non-empty, non-heading line
    for (let i = startIdx; i < lines.length; i++) {
        const line = lines[i].trim();
        if (line.length === 0 || line.startsWith('#'))
            continue;
        return line.length > 80 ? line.slice(0, 77) + '...' : line;
    }
    return '(no description)';
}
function fitContentToBudget(content, budget) {
    if (budget <= 0)
        return '';
    const contentBytes = Buffer.byteLength(content, 'utf8');
    if (contentBytes <= budget)
        return content;
    // Use pre-processor for markdown content
    const lines = content.split('\n');
    const maxLines = Math.max(10, Math.floor((budget / contentBytes) * lines.length));
    const config = {
        ...getDefaultConfig(),
        maxLinesPerFile: maxLines,
        maxTotalContextLines: maxLines,
    };
    const input = [{ path: 'inline', content }];
    const results = preprocessContext(input, config);
    if (results.length > 0) {
        return results[0].content;
    }
    // Fallback: simple truncation
    const buf = Buffer.from(content, 'utf8');
    const truncated = buf.subarray(0, budget - 20).toString('utf8');
    const lastNewline = truncated.lastIndexOf('\n');
    if (lastNewline > 0) {
        return truncated.slice(0, lastNewline) + '\n[...truncated]';
    }
    return truncated + '\n[...truncated]';
}
// preprocessDocContent removed — no longer needed since docs are loaded by Claude CLI
function resolveProjectName(workspace) {
    const pkgPath = join(workspace, 'package.json');
    if (!existsSync(pkgPath))
        return workspace.split('/').pop() ?? 'project';
    try {
        const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
        return pkg.name ?? workspace.split('/').pop() ?? 'project';
    }
    catch {
        return workspace.split('/').pop() ?? 'project';
    }
}
//# sourceMappingURL=context-builder.js.map