/**
 * Web Tools for the Studiograph Agent
 *
 * Provides web search and page fetching capabilities.
 * Supports multiple search providers: Brave, Google, Tavily.
 * Provider is configured via workspace config or env vars.
 */
import { Type as T } from 'typebox';
import { tryResolve } from '../../core/secrets/read-flip.js';
function wrapToolResult(result) {
    const text = JSON.stringify(result, null, 2);
    return {
        content: [{ type: 'text', text }],
        details: result,
    };
}
// ── Provider: Brave Search ─────────────────────────────────────
class BraveSearchProvider {
    apiKey;
    name = 'brave';
    constructor(apiKey) {
        this.apiKey = apiKey;
    }
    async search(query, maxResults) {
        const params = new URLSearchParams({ q: query, count: String(maxResults) });
        const res = await fetch(`https://api.search.brave.com/res/v1/web/search?${params}`, {
            headers: { 'X-Subscription-Token': this.apiKey, 'Accept': 'application/json' },
        });
        if (!res.ok)
            throw new Error(`Brave search failed: ${res.status}`);
        const data = await res.json();
        return (data.web?.results ?? []).slice(0, maxResults).map((r) => ({
            title: r.title ?? '',
            url: r.url ?? '',
            snippet: r.description ?? '',
        }));
    }
}
// ── Provider: Google Custom Search ─────────────────────────────
class GoogleSearchProvider {
    apiKey;
    cx;
    name = 'google';
    constructor(apiKey, cx) {
        this.apiKey = apiKey;
        this.cx = cx;
    }
    async search(query, maxResults) {
        const num = Math.min(maxResults, 10);
        const params = new URLSearchParams({ key: this.apiKey, cx: this.cx, q: query, num: String(num) });
        const res = await fetch(`https://www.googleapis.com/customsearch/v1?${params}`);
        if (!res.ok)
            throw new Error(`Google search failed: ${res.status}`);
        const data = await res.json();
        return (data.items ?? []).slice(0, maxResults).map((r) => ({
            title: r.title ?? '',
            url: r.link ?? '',
            snippet: r.snippet ?? '',
        }));
    }
}
// ── Provider: Tavily ───────────────────────────────────────────
class TavilySearchProvider {
    apiKey;
    name = 'tavily';
    constructor(apiKey) {
        this.apiKey = apiKey;
    }
    async search(query, maxResults) {
        const res = await fetch('https://api.tavily.com/search', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                api_key: this.apiKey,
                query,
                max_results: maxResults,
                search_depth: 'basic',
            }),
        });
        if (!res.ok)
            throw new Error(`Tavily search failed: ${res.status}`);
        const data = await res.json();
        return (data.results ?? []).slice(0, maxResults).map((r) => ({
            title: r.title ?? '',
            url: r.url ?? '',
            snippet: r.content ?? '',
        }));
    }
}
/**
 * Resolve a search provider from config + env vars.
 *
 * Phase 6 read-flip: the encrypted store wins via the resolver. We
 * check the store for each provider's api_key + (for Google)
 * `search.google.cx`, then fall back to the workspace config and
 * env-var chain. The resolver itself bakes in env > store > default
 * precedence, so calling tryResolve() is the right way to consult
 * either source without duplicating precedence logic.
 *
 * Env vars checked (legacy fallback only):
 *   BRAVE_SEARCH_API_KEY
 *   GOOGLE_SEARCH_API_KEY + GOOGLE_SEARCH_CX
 *   TAVILY_API_KEY
 */
function resolveSearchProvider(config) {
    // Three-tier resolution, in order of precedence:
    //   1. `search.provider` setting in the new store — explicit, picker-driven.
    //      Uses ONLY that provider's key. Other keys, even if stored, are
    //      ignored (no silent cascade-overrides-my-pick).
    //   2. Legacy workspace-config `search.provider` — same explicit behavior,
    //      kept for back-compat with old workspace.json files.
    //   3. Auto-detect cascade (brave → tavily → google) for any setup that
    //      predates the picker. First key found wins.
    const storeProvider = tryResolve('search.provider');
    const storeBrave = tryResolve('search.brave.api_key');
    const storeTavily = tryResolve('search.tavily.api_key');
    const storeGoogle = tryResolve('search.google.api_key');
    const storeGoogleCx = tryResolve('search.google.cx');
    // 1. Explicit picker via the new store. When the user has chosen a
    //    provider in the settings UI, that choice is authoritative — no
    //    cascade. If the chosen provider's key isn't set, we return null
    //    rather than silently fall through to a different provider.
    if (storeProvider === 'brave' || storeProvider === 'tavily' || storeProvider === 'google') {
        switch (storeProvider) {
            case 'brave': {
                const key = storeBrave || process.env.BRAVE_SEARCH_API_KEY;
                return key ? new BraveSearchProvider(key) : null;
            }
            case 'tavily': {
                const key = storeTavily || process.env.TAVILY_API_KEY;
                return key ? new TavilySearchProvider(key) : null;
            }
            case 'google': {
                const key = storeGoogle || process.env.GOOGLE_SEARCH_API_KEY;
                const cx = storeGoogleCx || process.env.GOOGLE_SEARCH_CX;
                return key && cx ? new GoogleSearchProvider(key, cx) : null;
            }
        }
    }
    // 2. Legacy workspace-config preference.
    if (config?.provider && config?.api_key) {
        const key = resolveEnvValue(config.api_key);
        switch (config.provider) {
            case 'brave':
                return new BraveSearchProvider(storeBrave || key);
            case 'google': {
                const cx = storeGoogleCx || resolveEnvValue(config.google_cx ?? '');
                if (!cx)
                    return null;
                return new GoogleSearchProvider(storeGoogle || key, cx);
            }
            case 'tavily':
                return new TavilySearchProvider(storeTavily || key);
        }
    }
    // 3. Auto-detect cascade (back-compat for installs without an explicit
    //    picker setting). Brave wins if any of its surfaces are set.
    if (storeBrave)
        return new BraveSearchProvider(storeBrave);
    if (process.env.BRAVE_SEARCH_API_KEY) {
        return new BraveSearchProvider(process.env.BRAVE_SEARCH_API_KEY);
    }
    if (storeTavily)
        return new TavilySearchProvider(storeTavily);
    if (process.env.TAVILY_API_KEY) {
        return new TavilySearchProvider(process.env.TAVILY_API_KEY);
    }
    if (storeGoogle && storeGoogleCx) {
        return new GoogleSearchProvider(storeGoogle, storeGoogleCx);
    }
    if (process.env.GOOGLE_SEARCH_API_KEY && process.env.GOOGLE_SEARCH_CX) {
        return new GoogleSearchProvider(process.env.GOOGLE_SEARCH_API_KEY, process.env.GOOGLE_SEARCH_CX);
    }
    return null;
}
/** Resolve $ENV_VAR references in config values */
function resolveEnvValue(value) {
    if (value.startsWith('$')) {
        return process.env[value.slice(1)] ?? '';
    }
    return value;
}
// ── Tool creation ──────────────────────────────────────────────
/**
 * Create web tools for the agent.
 * @param searchConfig Optional search provider config from workspace settings
 */
export function createWebTools(searchConfig) {
    const provider = resolveSearchProvider(searchConfig);
    const tools = [];
    // Only add web_search if a provider is available
    if (provider) {
        tools.push({
            name: 'web_search',
            label: 'Web Search',
            description: `Search the web for information (via ${provider.name}). Use this when the user asks about topics outside the knowledge graph — general knowledge, how-to questions, current events, external resources, etc.`,
            parameters: T.Object({
                query: T.String({ description: 'Search query' }),
                maxResults: T.Optional(T.Number({ description: 'Maximum results to return (default 5, max 10)' })),
            }),
            execute: async (_toolCallId, params) => {
                try {
                    const maxResults = Math.min(params.maxResults ?? 5, 10);
                    const results = await provider.search(params.query, maxResults);
                    if (results.length === 0) {
                        return wrapToolResult({ success: true, results: [], message: 'No results found.' });
                    }
                    return wrapToolResult({ success: true, provider: provider.name, results });
                }
                catch (err) {
                    return wrapToolResult({ success: false, error: err.message ?? 'Web search failed' });
                }
            },
        });
    }
    // web_fetch is always available (no API key needed)
    tools.push({
        name: 'web_fetch',
        label: 'Fetch Web Page',
        description: 'Fetch a web page and extract its readable text content. Use this to read articles, documentation, or any URL the user provides or that appears in search results.',
        parameters: T.Object({
            url: T.String({ description: 'URL to fetch' }),
            maxLength: T.Optional(T.Number({ description: 'Maximum characters to return (default 8000)' })),
        }),
        execute: async (_toolCallId, params) => {
            try {
                const maxLength = params.maxLength ?? 8000;
                const content = await fetchPage(params.url, maxLength);
                return wrapToolResult({ success: true, url: params.url, content });
            }
            catch (err) {
                return wrapToolResult({ success: false, error: err.message ?? 'Failed to fetch page' });
            }
        },
    });
    return tools;
}
// ── Page fetch implementation ──────────────────────────────────
async function fetchPage(url, maxLength) {
    const res = await fetch(url, {
        headers: {
            'User-Agent': 'Mozilla/5.0 (compatible; Studiograph/1.0)',
            'Accept': 'text/html,application/xhtml+xml,text/plain',
        },
        redirect: 'follow',
        signal: AbortSignal.timeout(10000),
    });
    if (!res.ok) {
        throw new Error(`Fetch failed with status ${res.status}`);
    }
    const contentType = res.headers.get('content-type') ?? '';
    if (contentType.includes('text/plain') || contentType.includes('application/json')) {
        const text = await res.text();
        return text.slice(0, maxLength);
    }
    const html = await res.text();
    return extractReadableText(html).slice(0, maxLength);
}
function extractReadableText(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, '')
        .replace(/<aside[\s\S]*?<\/aside>/gi, '');
    text = text.replace(/<\/?(p|div|br|h[1-6]|li|tr|blockquote|section|article)[^>]*>/gi, '\n');
    text = text.replace(/<[^>]+>/g, '');
    text = text
        .replace(/&amp;/g, '&')
        .replace(/&lt;/g, '<')
        .replace(/&gt;/g, '>')
        .replace(/&quot;/g, '"')
        .replace(/&#39;/g, "'")
        .replace(/&nbsp;/g, ' ');
    text = text
        .split('\n')
        .map(line => line.trim())
        .filter(line => line.length > 0)
        .join('\n');
    return text;
}
//# sourceMappingURL=web-tools.js.map