"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AgentModService = exports.classifyAgentError = void 0;
const crypto = __importStar(require("crypto"));
const promises_1 = __importDefault(require("fs/promises"));
const os_1 = __importDefault(require("os"));
const path_1 = __importDefault(require("path"));
const agentModPrompts_1 = require("../../prompts/agentModPrompts");
const logger_1 = require("../../utils/logger");
const AgentCore_1 = require("./AgentCore");
const log = (0, logger_1.createLogger)('AgentModService');
/** Prefix for spend limit errors, used by the frontend to detect and show upgrade UI */
const SPEND_LIMIT_ERROR_PREFIX = 'SPEND_LIMIT_EXCEEDED: ';
const MAX_TASK_CALLS_PER_REQUEST = 2;
const MAX_TOOL_TARGETS_PER_TYPE = 20;
const mergeToolCallCounts = (primary, secondary) => {
    const merged = { ...primary };
    for (const [toolName, count] of Object.entries(secondary)) {
        merged[toolName] = (merged[toolName] || 0) + count;
    }
    return merged;
};
const mergeToolTargetLists = (first, second) => {
    const merged = new Set([...first, ...second]);
    return Array.from(merged).slice(0, MAX_TOOL_TARGETS_PER_TYPE);
};
const mergeToolSummaries = (first, second) => {
    if (!first && !second) {
        return undefined;
    }
    if (!first) {
        return second;
    }
    if (!second) {
        return first;
    }
    return {
        toolCallCounts: mergeToolCallCounts(first.toolCallCounts, second.toolCallCounts),
        readTargets: mergeToolTargetLists(first.readTargets, second.readTargets),
        editTargets: mergeToolTargetLists(first.editTargets, second.editTargets),
        writeTargets: mergeToolTargetLists(first.writeTargets, second.writeTargets),
    };
};
/**
 * Classify raw agent errors into user-facing messages.
 * Spend-limit errors arrive already prefixed with SPEND_LIMIT_EXCEEDED from the
 * gateway auth layer and are passed through so the frontend can detect them.
 */
const classifyAgentError = (rawMessage) => {
    const lower = rawMessage.toLowerCase();
    if (lower.includes('timed out')) {
        return 'The request timed out. This can happen if the network is slow or if the request is very large.';
    }
    if (rawMessage.includes(SPEND_LIMIT_ERROR_PREFIX)) {
        return rawMessage;
    }
    if (lower.includes('401') || lower.includes('unauthorized')) {
        return 'Authentication expired. Please run `rivet logout` and `rivet login` to re-authenticate.';
    }
    if (lower.includes('403') || lower.includes('forbidden')) {
        return 'Access denied. Please contact support by emailing sam@rivet.design.';
    }
    return rawMessage;
};
exports.classifyAgentError = classifyAgentError;
/**
 * Service for agentic code modifications using Claude Agent SDK.
 * Agent autonomously searches for files, reads context, and makes modifications.
 * Uses AgentCore for shared infrastructure.
 */
class AgentModService {
    core;
    projectPath;
    constructor(projectPath, gatewayUrl) {
        this.core = new AgentCore_1.AgentCore(gatewayUrl);
        this.projectPath = projectPath;
    }
    /**
     * Execute an agentic modification workflow.
     * The agent autonomously uses Grep/Glob/Read to find files and Edit to modify them.
     *
     * @effect Streams progress events via onProgress callback
     * @param instruction User's modification instruction
     * @param initialElements Selected elements from UI with candidate file paths
     * @param onProgress Callback for streaming progress updates
     * @param designTokens Optional design system tokens
     * @param images Optional reference images
     * @param sessionId Optional session ID to resume a prior agent conversation
     * @param model Optional model ID to use (defaults to claude-opus-4-7)
     */
    async executeAgenticModification(instruction, initialElements, onProgress, designTokens, images, sessionId, model, correlation, telemetry) {
        log.info(`Starting agentic modification: "${instruction}"`);
        log.info(`Initial elements: ${initialElements.length}`);
        const tempImagePaths = [];
        try {
            // Save reference images to temp files so the agent can view them
            if (images && images.length > 0) {
                for (const image of images) {
                    const tempPath = await this.saveReferenceImage(image);
                    tempImagePaths.push(tempPath);
                    log.info(`Reference image saved to: ${tempPath}`);
                }
            }
            // Build lightweight initial context — file paths + tokens, no file content
            const initialContext = this.buildInitialContext(initialElements, designTokens, tempImagePaths);
            const buildOnProgress = (retryLabel) => {
                return (event) => {
                    if (event.type === 'error') {
                        const classified = (0, exports.classifyAgentError)(event.error || 'Unknown error');
                        onProgress({ type: 'error', error: classified });
                    }
                    else if (event.type === 'step') {
                        if (retryLabel && event.step?.type === 'reasoning') {
                            const stepData = event.step.data;
                            const rawText = stepData?.text || '';
                            if (rawText) {
                                onProgress({
                                    type: 'step',
                                    step: {
                                        ...event.step,
                                        data: { text: `[${retryLabel}] ${rawText}` },
                                    },
                                });
                                return;
                            }
                        }
                        onProgress({ type: 'step', step: event.step });
                    }
                };
            };
            // Execute strict-first: direct edits only, no Task orchestration.
            log.info('Running strict-first agent pass (Read/Edit/Write only)');
            const strictResult = await this.core.execute({
                systemPrompt: this.generateSystemPrompt('strict'),
                userPrompt: this.buildAgentPrompt(instruction, initialContext),
                allowedTools: ['Read', 'Edit', 'Write', 'Bash'],
                permissionMode: 'bypassPermissions',
                cwd: this.projectPath,
                resumeSessionId: sessionId,
                model,
                executionPolicy: {
                    disableBash: false,
                    disableSubagents: true,
                    maxTaskCalls: 0,
                },
                correlation,
                telemetry,
                onProgress: buildOnProgress(),
            });
            let result = strictResult;
            let mergedToolSummary = strictResult.toolSummary;
            if (strictResult.filesModified.length === 0) {
                log.warn('Strict-first pass modified 0 files. Retrying once with exploratory fallback.');
                const fallbackResult = await this.core.execute({
                    systemPrompt: this.generateSystemPrompt('fallback'),
                    userPrompt: `${this.buildAgentPrompt(instruction, initialContext)}\n\nNote: A direct-edit pass was attempted first and modified 0 files. Explore as needed.`,
                    allowedTools: ['Glob', 'Grep', 'Read', 'Edit', 'Write', 'Task', 'Bash'],
                    permissionMode: 'bypassPermissions',
                    cwd: this.projectPath,
                    resumeSessionId: strictResult.sessionId || sessionId,
                    model,
                    agents: {
                        'code-modifier': {
                            description: 'A subagent for making independent code modifications. Use this to parallelize multiple file edits that do not depend on each other.',
                            tools: ['Read', 'Edit', 'Write'],
                            prompt: this.generateSystemPrompt('subagent'),
                        },
                    },
                    executionPolicy: {
                        disableBash: false,
                        disableSubagents: true,
                        maxTaskCalls: MAX_TASK_CALLS_PER_REQUEST,
                        allowedTaskAgentTypes: ['code-modifier'],
                    },
                    correlation,
                    telemetry,
                    onProgress: buildOnProgress('fallback'),
                });
                result = {
                    ...fallbackResult,
                    ttfmMs: strictResult.ttfmMs ?? fallbackResult.ttfmMs,
                    ttfwMs: strictResult.ttfwMs ?? fallbackResult.ttfwMs,
                };
                mergedToolSummary = mergeToolSummaries(strictResult.toolSummary, fallbackResult.toolSummary);
            }
            log.info(`Agent completed. Modified ${result.filesModified.length} files`);
            const agenticResult = {
                success: true,
                filesModified: result.filesModified,
                reasoning: result.reasoning,
                changes: this.summarizeChanges(result.filesModified, result.reasoning),
                sessionId: result.sessionId,
                model: result.model,
                ttfmMs: result.ttfmMs,
                ttfwMs: result.ttfwMs,
                traceId: result.traceId,
                agentRunId: result.agentRunId,
                toolSummary: mergedToolSummary,
            };
            onProgress({ type: 'complete', result: agenticResult });
            return agenticResult;
        }
        catch (error) {
            log.error('Agentic modification failed:', error);
            if (error instanceof Error) {
                log.error('Error name:', error.name);
                log.error('Error message:', error.message);
                log.error('Error stack:', error.stack);
            }
            const errorMessage = (0, exports.classifyAgentError)(error instanceof Error ? error.message : 'Unknown error');
            onProgress({
                type: 'error',
                error: errorMessage,
            });
            return {
                success: false,
                filesModified: [],
                reasoning: [],
                changes: `Error: ${errorMessage}`,
            };
        }
        finally {
            // Clean up temp image files
            for (const tempPath of tempImagePaths) {
                try {
                    await promises_1.default.unlink(tempPath);
                    log.debug(`Cleaned up temp image: ${tempPath}`);
                }
                catch {
                    log.warn(`Failed to clean up temp image: ${tempPath}`);
                }
            }
        }
    }
    /**
     * Generate system prompt for the given pass mode.
     * 'strict'   — no Task tool, no subagent restriction (used for the direct-edit pass)
     * 'fallback' — Task tool available for parallel multi-file work
     * 'subagent' — no Task, restricted to provided context only
     */
    generateSystemPrompt(mode) {
        return (0, agentModPrompts_1.buildAgentSystemPrompt)(mode);
    }
    /**
     * Build the initial agent prompt with context
     */
    buildAgentPrompt(instruction, initialContext) {
        return (0, agentModPrompts_1.buildAgentUserPrompt)(this.projectPath, instruction, initialContext);
    }
    /**
     * Save a base64 data URL image to a temp file for agent access
     *
     * @effect Writes image to OS temp directory
     * @returns Absolute path to the saved temp file
     */
    async saveReferenceImage(dataUrl) {
        const match = dataUrl.match(/^data:image\/([\w+.-]+);base64,(.+)$/);
        if (!match) {
            throw new Error('Invalid image data URL format');
        }
        const mimeSubtype = match[1];
        const base64Data = match[2];
        const buffer = Buffer.from(base64Data, 'base64');
        // Map MIME subtypes to file extensions (e.g. svg+xml -> svg)
        const extension = mimeSubtype.includes('+')
            ? mimeSubtype.split('+')[0]
            : mimeSubtype;
        const filename = `rivet-ref-${crypto.randomBytes(4).toString('hex')}.${extension}`;
        const filePath = path_1.default.join(os_1.default.tmpdir(), filename);
        await promises_1.default.writeFile(filePath, buffer);
        return filePath;
    }
    /**
     * Format a compact single-line summary of a token category.
     * E.g. `border-radius: lg=0.5rem, md=calc(0.5rem - 2px)`
     */
    static formatTokenCategory(name, tokens) {
        const entries = [];
        for (const [key, value] of Object.entries(tokens)) {
            if (typeof value === 'string') {
                entries.push(`${key}=${value}`);
            }
            else {
                // Nested shades — flatten to key.shade=value
                for (const [shade, v] of Object.entries(value)) {
                    entries.push(`${key}.${shade}=${v}`);
                }
            }
        }
        return `${name}: ${entries.join(', ')}`;
    }
    /**
     * Build lightweight initial context — file paths + tokens only.
     * The agent reads file content itself via the Read tool, so we don't
     * pre-include it. This minimises input tokens and latency.
     */
    buildInitialContext(elements, designTokens, referenceImagePaths) {
        let context = 'CONTEXT:\n';
        // ── Framework ──
        if (designTokens?.framework) {
            context += `**Framework:** ${designTokens.framework}\n`;
        }
        // ── Propagate file paths from children to parents (on shallow copies) ──
        const enriched = elements.map((el) => ({ ...el }));
        for (const parent of enriched) {
            if (parent.filePaths && parent.filePaths.length > 0)
                continue;
            for (const child of enriched) {
                if (child === parent)
                    continue;
                if (child.filePaths &&
                    child.filePaths.length > 0 &&
                    child.xpath.startsWith(parent.xpath)) {
                    parent.filePaths = child.filePaths;
                    break;
                }
            }
        }
        // ── Unique files to edit ──
        const files = new Set();
        for (const el of enriched) {
            const f = el.filePaths?.[0]?.filePath;
            if (f)
                files.add(path_1.default.relative(this.projectPath, f));
        }
        if (files.size > 0) {
            context += `**Files to edit:** ${[...files].join(', ')}\n`;
        }
        // ── Compact design tokens ──
        if (designTokens?.registry) {
            const categories = Object.entries(designTokens.registry).filter(([, tokens]) => Object.keys(tokens).length > 0);
            if (categories.length > 0) {
                context += '**Design tokens** (use these instead of hardcoded values):\n';
                for (const [name, tokens] of categories) {
                    context += `  ${AgentModService.formatTokenCategory(name, tokens)}\n`;
                }
            }
        }
        // ── Reference images ──
        if (referenceImagePaths && referenceImagePaths.length > 0) {
            context += `**Reference images:**\n`;
            for (let i = 0; i < referenceImagePaths.length; i++) {
                context += `  ${i + 1}. ${referenceImagePaths[i]}\n`;
            }
            context += 'Read these images before making changes.\n';
        }
        context += '\nRead the target file(s), apply the changes, then summarize.';
        return context;
    }
    /**
     * Summarize changes for user display
     */
    summarizeChanges(filesModified, reasoning) {
        const fileList = filesModified
            .map((f) => `- ${path_1.default.basename(f)}`)
            .join('\n');
        const summary = reasoning.length > 0
            ? reasoning[reasoning.length - 1]
            : 'Completed modifications';
        return `Modified ${filesModified.length} file(s):\n${fileList}\n\n${summary}`;
    }
}
exports.AgentModService = AgentModService;
//# sourceMappingURL=AgentModService.js.map