import axios from 'axios';
import { getErrorMessage } from '../types/index.js';
import { logger } from '../utils/logger.js';
// ============================================================================
// Constants
// ============================================================================
const BRAVE_SEARCH_ENDPOINT = 'https://api.search.brave.com/res/v1/web/search';
const DEFAULT_PERPLEXITY_BASE_URL = 'https://openrouter.ai/api/v1';
const PERPLEXITY_DIRECT_BASE_URL = 'https://api.perplexity.ai';
const DEFAULT_PERPLEXITY_MODEL = 'perplexity/sonar-pro';
const DEFAULT_SEARCH_COUNT = 5;
const MAX_SEARCH_COUNT = 10;
const DEFAULT_TIMEOUT_MS = 20000;
const BRAVE_FRESHNESS_SHORTCUTS = new Set(['pd', 'pw', 'pm', 'py']);
const BRAVE_FRESHNESS_RANGE = /^(\d{4}-\d{2}-\d{2})to(\d{4}-\d{2}-\d{2})$/;
// ============================================================================
// Helpers
// ============================================================================
function normalizeFreshness(value) {
    if (!value)
        return undefined;
    const trimmed = value.trim().toLowerCase();
    if (!trimmed)
        return undefined;
    if (BRAVE_FRESHNESS_SHORTCUTS.has(trimmed))
        return trimmed;
    const match = value.trim().match(BRAVE_FRESHNESS_RANGE);
    if (match)
        return `${match[1]}to${match[2]}`;
    return undefined;
}
function resolveSiteName(url) {
    if (!url)
        return undefined;
    try {
        return new URL(url).hostname;
    }
    catch {
        return undefined;
    }
}
function resolvePerplexityBaseUrl(apiKey) {
    if (!apiKey)
        return DEFAULT_PERPLEXITY_BASE_URL;
    if (apiKey.startsWith('pplx-'))
        return PERPLEXITY_DIRECT_BASE_URL;
    return DEFAULT_PERPLEXITY_BASE_URL; // OpenRouter key or unknown
}
/**
 * Web Search Tool — OpenClaw-aligned provider chain
 *
 * Provider resolution (auto mode):
 *   Brave MCP → Brave API → Perplexity → Serper → DuckDuckGo
 *
 * Supports country, search_lang, ui_lang, freshness (Brave), Perplexity AI search.
 */
export class WebSearchTool {
    cache = new Map();
    perplexityCache = new Map();
    cacheTTL = 15 * 60 * 1000; // 15 minutes
    // API keys resolved once at construction
    serperApiKey;
    braveApiKey;
    perplexityApiKey;
    constructor() {
        this.serperApiKey = process.env.SERPER_API_KEY;
        this.braveApiKey = process.env.BRAVE_API_KEY;
        this.perplexityApiKey = process.env.PERPLEXITY_API_KEY || process.env.OPENROUTER_API_KEY;
        const providers = [];
        if (this.braveApiKey)
            providers.push('brave');
        if (this.perplexityApiKey)
            providers.push('perplexity');
        if (this.serperApiKey)
            providers.push('serper');
        providers.push('duckduckgo');
        logger.debug('Web search providers available', { providers });
    }
    // ============================================================================
    // Main search entry point
    // ============================================================================
    // Cache failed queries to avoid repeated timeouts (TTL: 2 minutes)
    failedQueries = new Map();
    static FAILED_QUERY_TTL = 120000;
    async search(query, options = {}) {
        const { maxResults = DEFAULT_SEARCH_COUNT } = options;
        const count = Math.max(1, Math.min(MAX_SEARCH_COUNT, maxResults));
        try {
            // Check cache
            const cacheKey = this.buildCacheKey(query, count, options);
            const cached = this.cache.get(cacheKey);
            if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
                return { success: true, output: this.formatResults(cached.results, query) };
            }
            // Check if this query recently failed (avoid wasting 20s+ on repeated timeouts)
            const failedAt = this.failedQueries.get(query);
            if (failedAt && Date.now() - failedAt < WebSearchTool.FAILED_QUERY_TTL) {
                return { success: false, error: 'Web search is unavailable (all providers failed recently). Do NOT retry — proceed using your own knowledge to complete the task.' };
            }
            // If forced provider
            if (options.provider) {
                return await this.searchWithProvider(options.provider, query, count, options, cacheKey);
            }
            // Auto fallback chain: Brave MCP → Brave API → Perplexity → Serper → DuckDuckGo
            const chain = this.buildProviderChain();
            let lastError;
            for (const provider of chain) {
                try {
                    return await this.searchWithProvider(provider, query, count, options, cacheKey);
                }
                catch (error) {
                    lastError = getErrorMessage(error);
                    logger.debug(`Search provider ${provider} failed, trying next`, { error: lastError });
                }
            }
            // Cache the failure to prevent repeated timeouts
            this.failedQueries.set(query, Date.now());
            return { success: false, error: `All search providers failed. Last error: ${lastError}. Do NOT retry web search — proceed using your own knowledge to complete the task.` };
        }
        catch (error) {
            return { success: false, error: `Web search failed: ${getErrorMessage(error)}` };
        }
    }
    /**
     * Perplexity AI search — returns synthesized answer with citations
     */
    async searchPerplexity(query, options = {}) {
        if (!this.perplexityApiKey) {
            return {
                success: false,
                error: 'Perplexity search requires PERPLEXITY_API_KEY or OPENROUTER_API_KEY.',
            };
        }
        const cacheKey = `perplexity:${query}`;
        const cached = this.perplexityCache.get(cacheKey);
        if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
            return { success: true, output: this.formatPerplexityResult(cached.result, query) };
        }
        const result = await this.runPerplexitySearch(query, options);
        this.perplexityCache.set(cacheKey, { result, timestamp: Date.now() });
        return { success: true, output: this.formatPerplexityResult(result, query) };
    }
    // ============================================================================
    // Provider chain
    // ============================================================================
    buildProviderChain() {
        const chain = [];
        // Brave MCP is checked dynamically
        chain.push('brave-mcp');
        if (this.braveApiKey)
            chain.push('brave');
        if (this.perplexityApiKey)
            chain.push('perplexity');
        if (this.serperApiKey)
            chain.push('serper');
        chain.push('duckduckgo');
        return chain;
    }
    async searchWithProvider(provider, query, count, options, cacheKey) {
        let results;
        switch (provider) {
            case 'brave-mcp':
                if (!(await this.isBraveMCPAvailable())) {
                    throw new Error('Brave MCP not connected');
                }
                results = await this.searchViaBraveMCP(query, count);
                break;
            case 'brave':
                results = await this.searchBraveAPI(query, count, options);
                break;
            case 'perplexity': {
                const pResult = await this.runPerplexitySearch(query, options);
                // Convert perplexity to SearchResult[] for caching/formatting uniformity
                results = pResult.citations.map((url, i) => ({
                    title: `Citation ${i + 1}`,
                    url,
                    snippet: i === 0 ? pResult.content.slice(0, 300) : '',
                    siteName: resolveSiteName(url),
                }));
                if (results.length === 0) {
                    results = [{ title: 'Perplexity Answer', url: '', snippet: pResult.content }];
                }
                break;
            }
            case 'serper':
                results = await this.searchSerper(query, count);
                break;
            case 'duckduckgo':
                results = await this.searchDuckDuckGo(query, count);
                break;
            default:
                throw new Error(`Unknown provider: ${provider}`);
        }
        if (results.length === 0) {
            return { success: true, output: `No results found for: "${query}"` };
        }
        this.cache.set(cacheKey, { results, timestamp: Date.now() });
        return { success: true, output: this.formatResults(results, query) };
    }
    buildCacheKey(query, count, options) {
        const parts = [
            options.provider || 'auto',
            query,
            count,
            options.country || 'default',
            options.search_lang || 'default',
            options.freshness || 'default',
        ];
        return parts.join(':');
    }
    // ============================================================================
    // Brave MCP
    // ============================================================================
    async isBraveMCPAvailable() {
        try {
            const { getMCPManager } = await import('../codebuddy/tools.js');
            const manager = getMCPManager();
            return manager.getServers().includes('brave-search');
        }
        catch {
            return false;
        }
    }
    async searchViaBraveMCP(query, maxResults) {
        const { getMCPManager } = await import('../codebuddy/tools.js');
        const manager = getMCPManager();
        const result = await manager.callTool('mcp__brave-search__brave_web_search', {
            query,
            count: maxResults,
        });
        const results = [];
        if (result.content) {
            for (const item of result.content) {
                if (item.type === 'text' && typeof item.text === 'string') {
                    try {
                        const parsed = JSON.parse(item.text);
                        const webResults = parsed.web?.results || parsed.results || [];
                        for (const r of webResults.slice(0, maxResults)) {
                            results.push({
                                title: r.title || '',
                                url: r.url || '',
                                snippet: r.description || r.snippet || '',
                                siteName: resolveSiteName(r.url),
                            });
                        }
                    }
                    catch {
                        results.push({ title: 'Brave Search Result', url: '', snippet: item.text });
                    }
                }
            }
        }
        logger.debug('Brave MCP search completed', { query, resultCount: results.length });
        return results;
    }
    // ============================================================================
    // Brave Direct API (OpenClaw-aligned)
    // ============================================================================
    async searchBraveAPI(query, count, options) {
        const url = new URL(BRAVE_SEARCH_ENDPOINT);
        url.searchParams.set('q', query);
        url.searchParams.set('count', String(count));
        if (options.country)
            url.searchParams.set('country', options.country);
        if (options.search_lang)
            url.searchParams.set('search_lang', options.search_lang);
        if (options.ui_lang)
            url.searchParams.set('ui_lang', options.ui_lang);
        const freshness = normalizeFreshness(options.freshness);
        if (freshness)
            url.searchParams.set('freshness', freshness);
        const response = await axios.get(url.toString(), {
            headers: {
                'Accept': 'application/json',
                'X-Subscription-Token': this.braveApiKey,
            },
            timeout: DEFAULT_TIMEOUT_MS,
        });
        const webResults = response.data.web?.results || [];
        const results = webResults.map((entry) => ({
            title: entry.title || '',
            url: entry.url || '',
            snippet: entry.description || '',
            siteName: resolveSiteName(entry.url),
            published: entry.age || undefined,
        }));
        logger.debug('Brave API search completed', { query, resultCount: results.length });
        return results;
    }
    // ============================================================================
    // Perplexity (OpenClaw-aligned: direct or via OpenRouter)
    // ============================================================================
    async runPerplexitySearch(query, _options) {
        const apiKey = this.perplexityApiKey;
        const baseUrl = resolvePerplexityBaseUrl(apiKey);
        const model = process.env.PERPLEXITY_MODEL || DEFAULT_PERPLEXITY_MODEL;
        const endpoint = `${baseUrl.replace(/\/$/, '')}/chat/completions`;
        const response = await axios.post(endpoint, {
            model,
            messages: [{ role: 'user', content: query }],
        }, {
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${apiKey}`,
                'HTTP-Referer': 'https://github.com/code-buddy',
                'X-Title': 'Code Buddy Web Search',
            },
            timeout: DEFAULT_TIMEOUT_MS,
        });
        const content = response.data.choices?.[0]?.message?.content ?? 'No response';
        const citations = response.data.citations ?? [];
        logger.debug('Perplexity search completed', { query, model, citationCount: citations.length });
        return { content, citations, model };
    }
    // ============================================================================
    // Serper (Google Search)
    // ============================================================================
    async searchSerper(query, maxResults) {
        const response = await axios.post('https://google.serper.dev/search', { q: query, num: maxResults }, {
            headers: {
                'X-API-KEY': this.serperApiKey,
                'Content-Type': 'application/json',
            },
            timeout: DEFAULT_TIMEOUT_MS,
        });
        const results = [];
        if (response.data.answerBox?.answer) {
            results.push({
                title: response.data.answerBox.title || 'Answer',
                url: '',
                snippet: response.data.answerBox.answer,
            });
        }
        if (response.data.knowledgeGraph?.description) {
            results.push({
                title: response.data.knowledgeGraph.title || 'Knowledge',
                url: '',
                snippet: response.data.knowledgeGraph.description,
            });
        }
        if (response.data.organic) {
            for (const result of response.data.organic.slice(0, maxResults)) {
                results.push({
                    title: result.title,
                    url: result.link,
                    snippet: result.snippet,
                    siteName: resolveSiteName(result.link),
                });
            }
        }
        logger.debug('Serper search completed', { query, resultCount: results.length });
        return results;
    }
    // ============================================================================
    // DuckDuckGo (ultimate fallback, no API key needed)
    // ============================================================================
    async searchDuckDuckGo(query, maxResults) {
        const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
        const response = await axios.get(searchUrl, {
            headers: {
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
                'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
                'Accept-Language': 'en-US,en;q=0.5',
            },
            timeout: DEFAULT_TIMEOUT_MS,
        });
        // Cap HTML size to prevent regex backtracking on very large pages
        const html = typeof response.data === 'string' && response.data.length > 2_000_000
            ? response.data.slice(0, 2_000_000)
            : response.data;
        const results = [];
        const resultRegex = /<div[^>]*class="[^"]*result[^"]*"[^>]*>([\s\S]*?)<\/div>\s*<\/div>/gi;
        const titleRegex = /<a[^>]*class="[^"]*result__a[^"]*"[^>]*href="([^"]*)"[^>]*>([^<]*)<\/a>/i;
        const snippetRegex = /<a[^>]*class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/a>/i;
        let match;
        while ((match = resultRegex.exec(html)) !== null && results.length < maxResults) {
            const resultHtml = match[1];
            const titleMatch = titleRegex.exec(resultHtml);
            const snippetMatch = snippetRegex.exec(resultHtml);
            if (titleMatch) {
                let url = titleMatch[1];
                if (url.includes('uddg=')) {
                    const uddgMatch = url.match(/uddg=([^&]+)/);
                    if (uddgMatch)
                        url = decodeURIComponent(uddgMatch[1]);
                }
                results.push({
                    title: this.decodeHtmlEntities(titleMatch[2].trim()),
                    url,
                    snippet: snippetMatch
                        ? this.decodeHtmlEntities(this.stripHtml(snippetMatch[1]).trim())
                        : '',
                    siteName: resolveSiteName(url),
                });
            }
        }
        // Fallback parsing
        if (results.length === 0) {
            const linkRegex = /<a[^>]*class="[^"]*result__url[^"]*"[^>]*href="([^"]*)"[^>]*>([^<]*)<\/a>/gi;
            while ((match = linkRegex.exec(html)) !== null && results.length < maxResults) {
                let url = match[1];
                if (url.includes('uddg=')) {
                    const uddgMatch = url.match(/uddg=([^&]+)/);
                    if (uddgMatch)
                        url = decodeURIComponent(uddgMatch[1]);
                }
                results.push({
                    title: this.decodeHtmlEntities(match[2].trim()) || url,
                    url,
                    snippet: '',
                    siteName: resolveSiteName(url),
                });
            }
        }
        return results;
    }
    // ============================================================================
    // Fetch page
    // ============================================================================
    async fetchPage(url, _prompt) {
        try {
            const response = await axios.get(url, {
                headers: {
                    'User-Agent': 'Mozilla/5.0 (compatible; CodeBuddyCLI/1.0; +https://github.com/code-buddy)',
                    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
                },
                timeout: DEFAULT_TIMEOUT_MS,
                maxRedirects: 5,
            });
            const html = response.data;
            const text = this.extractTextFromHtml(html);
            const maxLength = 8000;
            const truncatedText = text.length > maxLength
                ? text.substring(0, maxLength) + '\n\n[Content truncated...]'
                : text;
            return {
                success: true,
                output: `Content from ${url}:\n\n${truncatedText}`,
                data: { url, contentLength: text.length },
            };
        }
        catch (error) {
            return { success: false, error: `Failed to fetch page: ${getErrorMessage(error)}` };
        }
    }
    // ============================================================================
    // Formatting
    // ============================================================================
    formatPerplexityResult(result, query) {
        const lines = [];
        lines.push(`\nPerplexity Search: "${query}" (${result.model})`);
        lines.push('='.repeat(50));
        lines.push('');
        lines.push(result.content);
        if (result.citations.length > 0) {
            lines.push('');
            lines.push('Citations:');
            result.citations.forEach((url, i) => {
                lines.push(`  ${i + 1}. ${url}`);
            });
        }
        lines.push('');
        lines.push('-'.repeat(50));
        return lines.join('\n');
    }
    isWeatherQuery(query) {
        const weatherKeywords = ['météo', 'meteo', 'weather', 'température', 'temperature', 'forecast', 'prévisions'];
        const q = query.toLowerCase();
        return weatherKeywords.some(kw => q.includes(kw));
    }
    getWeatherEmoji(text) {
        const t = text.toLowerCase();
        if (t.includes('soleil') || t.includes('sunny') || t.includes('ensoleillé'))
            return '☀️';
        if (t.includes('pluie') || t.includes('rain') || t.includes('averse'))
            return '🌧️';
        if (t.includes('nuage') || t.includes('cloud') || t.includes('couvert'))
            return '☁️';
        if (t.includes('neige') || t.includes('snow'))
            return '❄️';
        if (t.includes('orage') || t.includes('thunder') || t.includes('storm'))
            return '⛈️';
        if (t.includes('brouillard') || t.includes('fog'))
            return '🌫️';
        if (t.includes('vent') || t.includes('wind'))
            return '💨';
        if (t.includes('éclaircies') || t.includes('partly'))
            return '⛅';
        return '🌡️';
    }
    formatWeatherResults(results, query) {
        const lines = [];
        const location = query.replace(/météo|meteo|weather/gi, '').trim();
        lines.push(`\n🌍 Météo ${location || 'actuelle'}`);
        lines.push('═'.repeat(40));
        lines.push('');
        for (const result of results.slice(0, 4)) {
            const emoji = this.getWeatherEmoji(result.snippet);
            if (!result.url && result.snippet) {
                lines.push(`${emoji} ${result.title}: ${result.snippet}`);
                lines.push('');
            }
            else if (result.url) {
                lines.push(`${emoji} **${result.title}**`);
                if (result.snippet) {
                    const cleanSnippet = result.snippet.replace(/·/g, '|').replace(/\s+/g, ' ').trim();
                    lines.push(`   ${cleanSnippet}`);
                }
                lines.push(`   🔗 ${result.url}`);
                lines.push('');
            }
        }
        lines.push('─'.repeat(40));
        return lines.join('\n');
    }
    formatResults(results, query) {
        if (this.isWeatherQuery(query)) {
            return this.formatWeatherResults(results, query);
        }
        const lines = [];
        lines.push(`\n🔍 Résultats pour: "${query}"`);
        lines.push('═'.repeat(50));
        lines.push('');
        for (let i = 0; i < results.length; i++) {
            const result = results[i];
            const num = `${i + 1}.`;
            if (!result.url && result.snippet) {
                lines.push(`📌 ${result.title}`);
                lines.push(`   ${result.snippet}`);
            }
            else {
                lines.push(`${num} **${result.title}**`);
                if (result.snippet)
                    lines.push(`   ${result.snippet}`);
                if (result.url)
                    lines.push(`   🔗 ${result.url}`);
                if (result.published)
                    lines.push(`   📅 ${result.published}`);
            }
            lines.push('');
        }
        lines.push('─'.repeat(50));
        return lines.join('\n');
    }
    // ============================================================================
    // HTML helpers
    // ============================================================================
    extractTextFromHtml(html) {
        let text = html
            .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
            .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
            .replace(/<nav[^>]*>[\s\S]*?<\/nav>/gi, '')
            .replace(/<header[^>]*>[\s\S]*?<\/header>/gi, '')
            .replace(/<footer[^>]*>[\s\S]*?<\/footer>/gi, '');
        text = text
            .replace(/<\/p>/gi, '\n\n')
            .replace(/<br\s*\/?>/gi, '\n')
            .replace(/<\/div>/gi, '\n')
            .replace(/<\/h[1-6]>/gi, '\n\n')
            .replace(/<\/li>/gi, '\n')
            .replace(/<li[^>]*>/gi, '• ');
        text = this.stripHtml(text);
        text = this.decodeHtmlEntities(text);
        text = text.replace(/\n\s*\n\s*\n/g, '\n\n').replace(/[ \t]+/g, ' ').trim();
        return text;
    }
    stripHtml(html) {
        return html.replace(/<[^>]*>/g, '');
    }
    decodeHtmlEntities(text) {
        const entities = {
            '&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"',
            '&#39;': "'", '&apos;': "'", '&nbsp;': ' ', '&ndash;': '–',
            '&mdash;': '—', '&hellip;': '…', '&copy;': '©', '&reg;': '®', '&trade;': '™',
        };
        let result = text;
        for (const [entity, char] of Object.entries(entities)) {
            result = result.replace(new RegExp(entity, 'gi'), char);
        }
        result = result.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(parseInt(code, 10)));
        result = result.replace(/&#x([0-9a-f]+);/gi, (_, code) => String.fromCharCode(parseInt(code, 16)));
        return result;
    }
    clearCache() {
        this.cache.clear();
        this.perplexityCache.clear();
        this.failedQueries.clear();
    }
}
// ============================================================================
// Exported helpers for testing
// ============================================================================
export const __testing = {
    normalizeFreshness,
    resolveSiteName,
    resolvePerplexityBaseUrl,
};
//# sourceMappingURL=web-search.js.map