"use strict";
/**
 * agent-session-runner.ts
 *
 * Streaming variant of runAgent wired to a VIBN agent_sessions row.
 * After every LLM turn + tool call, it PATCHes the session in the VIBN DB
 * so the frontend can poll (and later WebSocket) the live output.
 *
 * Key differences from runAgent:
 *  - Accepts an `emit` callback instead of updating job-store
 *  - Accepts an `isStopped` check so the frontend can cancel mid-run
 *  - Tracks which files were written/modified for the changed_files panel
 *  - Calls vibn-frontend's PATCH /api/projects/[id]/agent/sessions/[sid]
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.runSessionAgent = runSessionAgent;
const child_process_1 = require("child_process");
const llm_1 = require("./llm");
const tools_1 = require("./tools");
const loader_1 = require("./prompts/loader");
const vibn_events_ingest_1 = require("./vibn-events-ingest");
const MAX_TURNS = 60;
// ── VIBN DB bridge ────────────────────────────────────────────────────────────
async function patchSession(opts, payload) {
    const url = `${opts.vibnApiUrl}/api/projects/${opts.projectId}/agent/sessions/${opts.sessionId}`;
    try {
        await fetch(url, {
            method: 'PATCH',
            headers: { 'Content-Type': 'application/json', 'x-agent-runner-secret': process.env.AGENT_RUNNER_SECRET ?? '' },
            body: JSON.stringify(payload),
        });
    }
    catch (err) {
        // Log but don't crash — output will be lost for this line but loop continues
        console.warn('[session-runner] PATCH failed:', err instanceof Error ? err.message : err);
    }
}
function now() {
    return new Date().toISOString();
}
// ── File change tracking ──────────────────────────────────────────────────────
const FILE_WRITE_TOOLS = new Set(['write_file', 'replace_in_file', 'create_file']);
function extractChangedFile(toolName, args, workspaceRoot, appPath) {
    if (!FILE_WRITE_TOOLS.has(toolName))
        return null;
    const rawPath = String(args.path ?? args.file_path ?? '');
    if (!rawPath)
        return null;
    // Make path relative to appPath for display
    const fullPrefix = `${workspaceRoot}/${appPath}/`;
    const appPrefix = `${appPath}/`;
    let displayPath = rawPath
        .replace(fullPrefix, '')
        .replace(appPrefix, '');
    const fileStatus = toolName === 'write_file' ? 'added' : 'modified';
    return { path: displayPath, status: fileStatus };
}
// ── Auto-commit helper ────────────────────────────────────────────────────────
async function autoCommitAndDeploy(opts, task, emit) {
    const repoRoot = opts.repoRoot;
    if (!repoRoot || !opts.giteaRepo) {
        await emit({ ts: now(), type: 'info', text: 'Auto-approve skipped — no repo root available.' });
        return;
    }
    const gitOpts = { cwd: repoRoot, stdio: 'pipe' };
    const giteaApiUrl = process.env.GITEA_API_URL || '';
    const giteaUsername = process.env.GITEA_USERNAME || 'agent';
    const giteaToken = process.env.GITEA_API_TOKEN || '';
    try {
        try {
            (0, child_process_1.execSync)('git config user.email "agent@vibnai.com"', gitOpts);
            (0, child_process_1.execSync)('git config user.name "VIBN Agent"', gitOpts);
        }
        catch { /* already set */ }
        (0, child_process_1.execSync)('git add -A', gitOpts);
        const status = (0, child_process_1.execSync)('git status --porcelain', gitOpts).toString().trim();
        if (!status) {
            await emit({ ts: now(), type: 'info', text: '✓ No file changes to commit.' });
            await patchSession(opts, { status: 'approved' });
            return;
        }
        const commitMsg = `agent: ${task.slice(0, 72)}`;
        (0, child_process_1.execSync)(`git commit -m ${JSON.stringify(commitMsg)}`, gitOpts);
        await emit({ ts: now(), type: 'info', text: `✓ Committed: "${commitMsg}"` });
        const authedUrl = `${giteaApiUrl}/${opts.giteaRepo}.git`
            .replace('https://', `https://${giteaUsername}:${giteaToken}@`);
        (0, child_process_1.execSync)(`git push "${authedUrl}" HEAD:main`, gitOpts);
        await emit({ ts: now(), type: 'info', text: '✓ Pushed to Gitea.' });
        // Optional Coolify deploy
        let deployed = false;
        if (opts.coolifyApiUrl && opts.coolifyApiToken && opts.coolifyAppUuid) {
            try {
                const deployRes = await fetch(`${opts.coolifyApiUrl}/api/v1/applications/${opts.coolifyAppUuid}/start`, { method: 'POST', headers: { Authorization: `Bearer ${opts.coolifyApiToken}` } });
                deployed = deployRes.ok;
                if (deployed)
                    await emit({ ts: now(), type: 'info', text: '✓ Deployment triggered.' });
            }
            catch { /* best-effort */ }
        }
        await patchSession(opts, {
            status: 'approved',
            outputLine: {
                ts: now(), type: 'done',
                text: `✓ Auto-committed & ${deployed ? 'deployed' : 'pushed'}. No approval needed.`,
            },
        });
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        await emit({ ts: now(), type: 'error', text: `Auto-commit failed: ${msg}` });
        // Fall back to done so user can manually approve
        await patchSession(opts, { status: 'done' });
    }
}
// ── Main streaming execution loop ─────────────────────────────────────────────
async function runSessionAgent(config, task, ctx, opts) {
    const llm = (0, llm_1.createLLM)(config.model, { temperature: 0.2 });
    const oaiTools = (0, llm_1.toOAITools)(config.tools);
    const emit = async (line) => {
        console.log(`[session ${opts.sessionId}] ${line.type}: ${line.text}`);
        await Promise.all([
            patchSession(opts, { outputLine: line }),
            (0, vibn_events_ingest_1.ingestSessionEvents)(opts.vibnApiUrl, opts.projectId, opts.sessionId, [
                {
                    type: `output.${line.type}`,
                    payload: { text: line.text },
                    ts: line.ts,
                },
            ]),
        ]);
    };
    await emit({ ts: now(), type: 'info', text: `Agent starting (${llm.modelId}) — working in ${opts.appPath}` });
    // Scope the system prompt to the specific app within the monorepo
    const basePrompt = (0, loader_1.resolvePrompt)(config.promptId);
    const scopedPrompt = `${basePrompt}

## Active context
You are working inside the monorepo directory: ${opts.appPath}
All file paths you use should be relative to this directory unless otherwise specified.
When running commands, always cd into ${opts.appPath} first unless already there.
Do NOT run git commit or git push — the platform handles committing after you finish.
`;
    const history = [
        { role: 'user', content: task }
    ];
    let turn = 0;
    let finalText = '';
    const trackedFiles = new Map(); // path → status
    while (turn < MAX_TURNS) {
        // Check for stop signal between turns
        if (opts.isStopped()) {
            await emit({ ts: now(), type: 'info', text: 'Stopped by user.' });
            await patchSession(opts, { status: 'stopped' });
            return;
        }
        turn++;
        await emit({ ts: now(), type: 'info', text: `Turn ${turn} — thinking…` });
        const messages = [
            { role: 'system', content: scopedPrompt },
            ...history
        ];
        let response;
        try {
            response = await llm.chat(messages, oaiTools, 8192);
        }
        catch (err) {
            const msg = err instanceof Error ? err.message : String(err);
            await emit({ ts: now(), type: 'error', text: `LLM error: ${msg}` });
            await patchSession(opts, { status: 'failed', error: msg });
            return;
        }
        const assistantMsg = {
            role: 'assistant',
            content: response.content,
            tool_calls: response.tool_calls.length > 0 ? response.tool_calls : undefined
        };
        history.push(assistantMsg);
        // Agent finished — no more tool calls
        if (response.tool_calls.length === 0) {
            finalText = response.content ?? 'Task complete.';
            break;
        }
        // Execute each tool call
        for (const tc of response.tool_calls) {
            if (opts.isStopped())
                break;
            const fnName = tc.function.name;
            let fnArgs = {};
            try {
                fnArgs = JSON.parse(tc.function.arguments || '{}');
            }
            catch { /* bad JSON */ }
            // Human-readable step label
            const stepLabel = buildStepLabel(fnName, fnArgs);
            await emit({ ts: now(), type: 'step', text: stepLabel });
            let result;
            try {
                result = await (0, tools_1.executeTool)(fnName, fnArgs, ctx);
            }
            catch (err) {
                result = { error: err instanceof Error ? err.message : String(err) };
            }
            // Stream stdout/stderr if present
            if (result && typeof result === 'object') {
                const r = result;
                if (r.stdout && String(r.stdout).trim()) {
                    for (const line of String(r.stdout).split('\n').filter(Boolean).slice(0, 40)) {
                        await emit({ ts: now(), type: 'stdout', text: line });
                    }
                }
                if (r.stderr && String(r.stderr).trim()) {
                    for (const line of String(r.stderr).split('\n').filter(Boolean).slice(0, 20)) {
                        await emit({ ts: now(), type: 'stderr', text: line });
                    }
                }
                if (r.error) {
                    await emit({ ts: now(), type: 'error', text: String(r.error) });
                }
            }
            // Track file changes
            const changed = extractChangedFile(fnName, fnArgs, ctx.workspaceRoot, opts.appPath);
            if (changed && !trackedFiles.has(changed.path)) {
                trackedFiles.set(changed.path, changed.status);
                await patchSession(opts, { changedFile: changed });
                await emit({ ts: now(), type: 'info', text: `${changed.status === 'added' ? '+ Created' : '~ Modified'} ${changed.path}` });
            }
            history.push({
                role: 'tool',
                tool_call_id: tc.id,
                name: fnName,
                content: typeof result === 'string' ? result : JSON.stringify(result)
            });
        }
    }
    if (turn >= MAX_TURNS && !finalText) {
        finalText = `Hit the ${MAX_TURNS}-turn limit. Stopping.`;
    }
    await emit({ ts: now(), type: 'done', text: finalText });
    if (opts.autoApprove) {
        await autoCommitAndDeploy(opts, task, emit);
    }
    else {
        await patchSession(opts, {
            status: 'done',
            outputLine: { ts: now(), type: 'done', text: '✓ Complete — review changes and approve to commit.' },
        });
    }
}
// ── Step label helpers ────────────────────────────────────────────────────────
function buildStepLabel(tool, args) {
    switch (tool) {
        case 'read_file': return `Read ${args.path ?? args.file_path}`;
        case 'write_file': return `Write ${args.path ?? args.file_path}`;
        case 'replace_in_file': return `Edit ${args.path ?? args.file_path}`;
        case 'list_directory': return `List ${args.path ?? '.'}`;
        case 'find_files': return `Find files: ${args.pattern}`;
        case 'search_code': return `Search: ${args.query}`;
        case 'execute_command': return `Run: ${String(args.command ?? '').slice(0, 80)}`;
        case 'git_commit_and_push': return `Git commit: "${args.message}"`;
        default: return `${tool}(${JSON.stringify(args).slice(0, 60)})`;
    }
}
