/**
 * LLM Analysis tool — sends local files (images, PDFs, text) to a vision LLM
 * via the backend proxy for analysis/extraction.
 *
 * Output is always written to the agent temp directory.
 */
import { Type } from '@sinclair/typebox';
import { mkdir, readdir, readFile, stat, unlink, writeFile } from 'fs/promises';
import { basename, extname, join } from 'path';
import { getAgentTempDir } from '../../config.js';
import { LLM_ANALYSIS } from '../../tool-names.js';
import { mapShortcutLlmProxyError } from '../providers/shortcut-llm-proxy-client.js';
import { readShortcutSSE } from '../providers/shortcut-sse.js';
const TOOL_NAME = LLM_ANALYSIS;
const OUTPUT_DIR_NAME = 'llm_analysis';
const CLEANUP_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
/** MIME types for binary files sent as base64 image blocks. */
const IMAGE_EXTENSIONS = {
    '.png': 'image/png',
    '.jpg': 'image/jpeg',
    '.jpeg': 'image/jpeg',
    '.gif': 'image/gif',
    '.webp': 'image/webp'
};
/** Extensions treated as readable text (sent as text blocks). */
const TEXT_EXTENSIONS = new Set([
    '.txt',
    '.csv',
    '.tsv',
    '.json',
    '.jsonl',
    '.md',
    '.xml',
    '.yaml',
    '.yml',
    '.html',
    '.css',
    '.js',
    '.ts',
    '.py',
    '.sql',
    '.log',
    '.sh',
    '.cfg',
    '.ini',
    '.toml',
    '.env',
    '.bat',
    '.ps1',
    '.r',
    '.sas',
    '.do',
    '.vba',
    '.bas'
]);
const MODELS = {
    'gemini-3-flash-preview': 'gemini-3-flash-preview',
    'claude-haiku-4-5-20251001': 'claude-haiku-4-5-20251001'
};
const DEFAULT_MODEL = 'gemini-3-flash-preview';
const SYSTEM_PROMPT = `You are a document extraction assistant.

Extraction rules:
- Preserve data exactly: extracted numbers, formulas, formatting, and text must match the source precisely
- For structured data extractions such as tables
    - The delimiter used must be unambiguous and not included in the data itself. Use pipes \`|\` by default.
    - Make sure to handle empty values appropriately (e.g., "| |"). Skipping empty values will cause errors when programmatic extraction is done

Referencing:
- Ensure that extracted data is cited. For example, [Page X, Table "Name"] or [Page X, Section "Name"] before a dump of data blocks.
- IMPORTANT: Always cite using page numbers from the source document (on the bottom right of the page)`;
const TOOL_DESCRIPTION = `\
Analyzes files using a vision LLM (Gemini Flash or Claude Haiku). Sends files to the LLM and saves the analysis output to a temp file.

Supported image types: ${Object.keys(IMAGE_EXTENSIONS).join(', ')}
Supported text types: ${[...TEXT_EXTENSIONS].join(', ')}
Also supports: .pdf

MODEL SELECTION:
- "gemini-3-flash-preview" (default): Best for careful extraction, tables, and especially images
- "claude-haiku-4-5-20251001": Quick scanning, structure discovery

FOR EXTRACTION TASKS:
- Pay close attention to edge cases, such as commas, spaces, signs and null values.
- For financial data, parentheses are used to indicate negative values.

Output is saved to a temp file and the path is returned. Read the file to see the full output.`;
const schema = Type.Object({
    description: Type.String({
        description: "Brief human-readable description of what this analysis will do (e.g., 'Extracting sales data from Q4 report')"
    }),
    file_paths: Type.Array(Type.String(), {
        description: 'List of local file paths to analyze (e.g., ["C:/Users/me/report.pdf"]). Max 20.',
        maxItems: 20
    }),
    query: Type.String({
        description: 'The question/instruction for the LLM about the file content.'
    }),
    model: Type.Optional(Type.String({
        description: `Model to use: '${DEFAULT_MODEL}' (default, best for images/tables) or 'claude-haiku-4-5-20251001' (fallback/verifier)`
    })),
    output_filename: Type.Optional(Type.String({
        description: "Optional output filename (e.g., 'analysis.md'). Auto-generated if omitted."
    }))
});
/** Get (and create) the output directory for analysis results. */
async function getOutputDir() {
    const dir = join(getAgentTempDir(), OUTPUT_DIR_NAME);
    await mkdir(dir, { recursive: true });
    return dir;
}
/** Clean up files older than CLEANUP_MAX_AGE_MS. Best-effort, never throws. */
async function cleanupOldFiles(dir) {
    try {
        const entries = await readdir(dir);
        const now = Date.now();
        for (const entry of entries) {
            try {
                const filePath = join(dir, entry);
                const fileStat = await stat(filePath);
                if (fileStat.isFile() && now - fileStat.mtimeMs > CLEANUP_MAX_AGE_MS) {
                    await unlink(filePath);
                }
            }
            catch {
                // skip individual file errors
            }
        }
    }
    catch {
        // directory might not exist yet
    }
}
/**
 * Build the mixed text/image payload for the proxy request.
 *
 * Binary inputs are sent as base64 image_url blocks because the proxy exposes
 * an OpenAI-compatible content shape; readable text files stay inline so the
 * model can cite filenames while extracting.
 */
async function buildContent(filePaths, query) {
    const contentBlocks = [];
    for (const filePath of filePaths) {
        const ext = extname(filePath).toLowerCase();
        if (ext === '.pdf') {
            // PDF: send as base64 document source
            const buffer = await readFile(filePath);
            const base64 = buffer.toString('base64');
            contentBlocks.push({
                type: 'image_url',
                image_url: { url: `data:application/pdf;base64,${base64}` }
            });
        }
        else if (IMAGE_EXTENSIONS[ext]) {
            // Image: send as base64 image
            const buffer = await readFile(filePath);
            const base64 = buffer.toString('base64');
            contentBlocks.push({
                type: 'image_url',
                image_url: { url: `data:${IMAGE_EXTENSIONS[ext]};base64,${base64}` }
            });
        }
        else if (TEXT_EXTENSIONS.has(ext)) {
            // Text: read and include as text block
            const buffer = await readFile(filePath);
            const text = buffer.toString('utf-8');
            contentBlocks.push({
                type: 'text',
                text: `--- File: ${basename(filePath)} ---\n${text}`
            });
        }
        else {
            // Unknown extension: try as text
            const buffer = await readFile(filePath);
            const text = buffer.toString('utf-8');
            contentBlocks.push({
                type: 'text',
                text: `--- File: ${basename(filePath)} ---\n${text}`
            });
        }
    }
    // Add the query
    if (filePaths.length > 0) {
        const filesList = filePaths.map((fp) => `- ${basename(fp)}`).join('\n');
        contentBlocks.push({
            type: 'text',
            text: `Query: ${query}\n\nFiles:\n${filesList}`
        });
    }
    else {
        contentBlocks.push({ type: 'text', text: query });
    }
    return [
        { role: 'system', content: SYSTEM_PROMPT },
        { role: 'human', content: contentBlocks }
    ];
}
export function createLlmAnalysisTool() {
    return {
        name: TOOL_NAME,
        label: 'LLM Analysis',
        isReadOnly: true,
        description: TOOL_DESCRIPTION,
        parameters: schema,
        async execute(toolCallId, { description: _description, file_paths, query, model, output_filename }, signal, _onUpdate, ctx) {
            const selectedModel = (model && model in MODELS ? model : DEFAULT_MODEL);
            // Get output dir and clean old files
            const outputDir = await getOutputDir();
            await cleanupOldFiles(outputDir);
            const extCtx = ctx;
            if (!extCtx?.shortcutLlmProxy) {
                throw new Error('Not authenticated. Please log in first.');
            }
            // Build LLM messages
            const messages = await buildContent(file_paths, query);
            // Call the backend /api/llm/stream endpoint
            let response;
            try {
                response = await extCtx.shortcutLlmProxy.stream({
                    model: MODELS[selectedModel],
                    messages,
                    effort_level: 'low',
                    thinking_type: 'off'
                }, { signal });
            }
            catch (e) {
                throw mapShortcutLlmProxyError(e);
            }
            let text = '';
            await readShortcutSSE(response, async (event, payload) => {
                switch (event) {
                    case 'text_delta':
                        text +=
                            typeof payload?.delta === 'string'
                                ? payload.delta
                                : '';
                        break;
                    case 'error':
                        throw new Error(payload?.message || 'LLM stream failed');
                    default:
                        break;
                }
            });
            if (!text) {
                throw new Error('LLM returned empty response');
            }
            // Persist large analysis output out-of-band so the agent context only
            // receives a stable path and can read the file when needed.
            const filename = output_filename || `analysis_${toolCallId.slice(-8)}_${Date.now()}.md`;
            const outputPath = join(outputDir, filename);
            await writeFile(outputPath, text, 'utf-8');
            return {
                content: [
                    {
                        type: 'text',
                        text: `Analysis complete. Output saved to: ${outputPath}\n\nUse the read tool to view the full output.`
                    }
                ],
                details: undefined
            };
        }
    };
}
//# sourceMappingURL=llm-analysis.js.map