/**
 * Web Search tool for Clawx.
 *
 * Searches the web and returns results with titles, URLs, and snippets.
 *
 * Search backends (in priority order):
 * 1. SearXNG — self-hosted meta search engine (configure SEARXNG_URL env var)
 * 2. DuckDuckGo HTML — no API key required, scrapes lite.duckduckgo.com
 *
 * For best results, self-host SearXNG: docker run -p 8888:8080 searxng/searxng
 * Then set SEARXNG_URL=http://localhost:8888 in your environment.
 */
import { Type } from "@sinclair/typebox";
import { log } from "../utils/logger.js";
const WebSearchSchema = Type.Object({
    query: Type.String({ description: "Search query" }),
    max_results: Type.Optional(Type.Number({ description: "Maximum number of results to return (default: 10)" })),
});
/** Search via self-hosted SearXNG instance */
async function searchSearXNG(query, maxResults, baseUrl) {
    const url = `${baseUrl.replace(/\/$/, '')}/search?q=${encodeURIComponent(query)}&format=json&categories=general&language=en`;
    const response = await fetch(url, {
        headers: {
            'Accept': 'application/json',
            'User-Agent': 'Clawx/0.7',
        },
    });
    if (!response.ok) {
        throw new Error(`SearXNG returned HTTP ${response.status}`);
    }
    const data = await response.json();
    const results = [];
    for (const item of (data.results || [])) {
        if (results.length >= maxResults)
            break;
        results.push({
            title: item.title || '',
            url: item.url || '',
            snippet: item.content || '',
        });
    }
    return results;
}
/** Search via DuckDuckGo HTML (lite version, no API key) */
async function searchDuckDuckGo(query, maxResults) {
    const url = `https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`;
    const response = await fetch(url, {
        headers: {
            'User-Agent': 'Mozilla/5.0 (compatible; Clawx/0.7; +https://github.com/stevenmcsorley/clawx)',
            'Accept': 'text/html',
        },
    });
    if (!response.ok) {
        throw new Error(`DuckDuckGo returned HTTP ${response.status}`);
    }
    const html = await response.text();
    const results = [];
    // Parse DuckDuckGo lite results
    // Results are in table rows with class "result-link" for links and "result-snippet" for snippets
    const linkPattern = /<a[^>]+class="result-link"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
    const snippetPattern = /<td[^>]+class="result-snippet"[^>]*>([\s\S]*?)<\/td>/gi;
    const links = [];
    const snippets = [];
    let match;
    while ((match = linkPattern.exec(html)) !== null) {
        links.push({
            url: match[1].replace(/&amp;/g, '&'),
            title: match[2].replace(/<[^>]+>/g, '').trim(),
        });
    }
    while ((match = snippetPattern.exec(html)) !== null) {
        snippets.push(match[1].replace(/<[^>]+>/g, '').replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').trim());
    }
    // If the lite parsing didn't work well, try a more general approach
    if (links.length === 0) {
        // Try parsing the standard result format
        const generalPattern = /<a[^>]+rel="nofollow"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi;
        while ((match = generalPattern.exec(html)) !== null) {
            const href = match[1].replace(/&amp;/g, '&');
            const title = match[2].replace(/<[^>]+>/g, '').trim();
            if (href.startsWith('http') && title.length > 0 && !href.includes('duckduckgo.com')) {
                links.push({ url: href, title });
            }
        }
    }
    for (let i = 0; i < Math.min(links.length, maxResults); i++) {
        results.push({
            title: links[i].title,
            url: links[i].url,
            snippet: snippets[i] || '',
        });
    }
    return results;
}
export function createWebSearchTool() {
    return {
        name: "web_search",
        label: "Web Search",
        description: "Search the web and return results with titles, URLs, and snippets. " +
            "Uses SearXNG (self-hosted, set SEARXNG_URL env var) or DuckDuckGo as fallback. " +
            "Use web_read to fetch full content from result URLs.",
        parameters: WebSearchSchema,
        async execute(_toolCallId, params) {
            const maxResults = params.max_results || 10;
            const start = Date.now();
            let backend = 'unknown';
            log.info(`web_search: "${params.query}"`);
            try {
                let results = [];
                // Try SearXNG first if configured
                const searxngUrl = process.env.SEARXNG_URL;
                if (searxngUrl) {
                    try {
                        backend = 'searxng';
                        results = await searchSearXNG(params.query, maxResults, searxngUrl);
                        log.info(`web_search: SearXNG returned ${results.length} results`);
                    }
                    catch (error) {
                        log.warn(`web_search: SearXNG failed (${error instanceof Error ? error.message : error}), falling back to DuckDuckGo`);
                        results = [];
                    }
                }
                // Fall back to DuckDuckGo
                if (results.length === 0) {
                    backend = 'duckduckgo';
                    results = await searchDuckDuckGo(params.query, maxResults);
                    log.info(`web_search: DuckDuckGo returned ${results.length} results`);
                }
                const durationMs = Date.now() - start;
                if (results.length === 0) {
                    return {
                        content: [{ type: "text", text: `No results found for "${params.query}" (searched via ${backend})` }],
                        details: { query: params.query, backend, resultCount: 0, durationMs },
                    };
                }
                // Format results
                const formatted = results.map((r, i) => `${i + 1}. ${r.title}\n   ${r.url}\n   ${r.snippet}`).join('\n\n');
                return {
                    content: [{
                            type: "text",
                            text: `Search results for "${params.query}" (${results.length} results via ${backend}):\n\n${formatted}`,
                        }],
                    details: { query: params.query, backend, resultCount: results.length, durationMs },
                };
            }
            catch (error) {
                const durationMs = Date.now() - start;
                const message = error instanceof Error ? error.message : String(error);
                return {
                    content: [{ type: "text", text: `Search failed for "${params.query}": ${message}` }],
                    details: { query: params.query, backend, resultCount: 0, durationMs },
                };
            }
        },
    };
}
//# sourceMappingURL=webSearch.js.map