/**
 * Web Search Tool
 *
 * Searches the web using DuckDuckGo's HTML search.
 * Supports filtering by allowed/blocked domains.
 */
import * as cheerio from "cheerio";
import { z } from "zod";
import { getErrorMeta, getLogger } from "../../../../util/logger.js";
import { createToolFactory, PermissionLevel, ToolCategory } from "../types.js";
/**
 * Headers to mimic a browser request.
 */
const BROWSER_HEADERS = {
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
    Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.9",
    "Accept-Encoding": "gzip, deflate, br",
    "Cache-Control": "no-cache",
    Pragma: "no-cache",
    "Sec-Ch-Ua": '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
    "Sec-Ch-Ua-Mobile": "?0",
    "Sec-Ch-Ua-Platform": '"macOS"',
    "Sec-Fetch-Dest": "document",
    "Sec-Fetch-Mode": "navigate",
    "Sec-Fetch-Site": "none",
    "Sec-Fetch-User": "?1",
    "Upgrade-Insecure-Requests": "1",
};
/**
 * Extracts the actual URL from a DuckDuckGo redirect URL.
 *
 * DuckDuckGo wraps URLs in their redirect format like:
 * //duckduckgo.com/l/?uddg=https%3A%2F%2Fexample.com&rut=...
 *
 * @param ddgUrl - The DuckDuckGo redirect URL
 * @returns The actual destination URL, or the original URL if parsing fails
 */
function extractUrlFromDdgRedirect(ddgUrl) {
    try {
        // Handle relative URLs starting with //
        const fullUrl = ddgUrl.startsWith("//") ? `https:${ddgUrl}` : ddgUrl;
        const url = new URL(fullUrl);
        const uddg = url.searchParams.get("uddg");
        if (uddg) {
            return decodeURIComponent(uddg);
        }
        return ddgUrl;
    }
    catch (error) {
        getLogger().debug("Failed to parse DDG redirect URL", { ddgUrl, error });
        return ddgUrl;
    }
}
/**
 * Parses DuckDuckGo HTML search results and extracts search results.
 *
 * @param html - The HTML content from DuckDuckGo search
 * @returns Array of search results
 */
function parseDuckDuckGoResults(html) {
    const $ = cheerio.load(html);
    const results = [];
    // DuckDuckGo results are in div.result elements
    // Each result has:
    // - h2.result__title containing a.result__a (title and link)
    // - a.result__snippet (description)
    $("div.result").each((_index, element) => {
        const $el = $(element);
        // Skip ads (they have badge--ad class)
        if ($el.find(".badge--ad").length > 0) {
            return;
        }
        // Find the title link
        const $titleLink = $el.find("h2.result__title a.result__a").first();
        const title = $titleLink.text().trim();
        const rawLink = $titleLink.attr("href") || "";
        // Extract the actual URL from DuckDuckGo's redirect
        const link = extractUrlFromDdgRedirect(rawLink);
        // Find the snippet
        const snippet = $el.find("a.result__snippet").first().text().trim();
        // Only add valid results
        if (title && link && link.startsWith("http")) {
            results.push({ title, link, snippet });
        }
    });
    return results;
}
/**
 * Filters results by allowed and blocked domains.
 *
 * @param results - Array of search results
 * @param allowedDomains - Only include results from these domains (if provided)
 * @param blockedDomains - Exclude results from these domains
 * @returns Filtered results
 */
function filterByDomains(results, allowedDomains, blockedDomains) {
    return results.filter((result) => {
        try {
            const domain = new URL(result.link).hostname.toLowerCase();
            // Check blocked domains first
            if (blockedDomains?.length) {
                const isBlocked = blockedDomains.some((blocked) => domain.includes(blocked.toLowerCase()));
                if (isBlocked)
                    return false;
            }
            // Check allowed domains (if specified)
            if (allowedDomains?.length) {
                const isAllowed = allowedDomains.some((allowed) => domain.includes(allowed.toLowerCase()));
                if (!isAllowed)
                    return false;
            }
            return true;
        }
        catch {
            // Invalid URL, exclude it
            return false;
        }
    });
}
/**
 * Web search tool factory.
 *
 * Searches the web using DuckDuckGo's HTML search.
 */
export const webSearchToolFactory = createToolFactory("web_search", () => ({
    description: `
Searches the web and returns results. Use this tool PROACTIVELY when the user's question likely requires current information.

WHEN TO USE (proactively search when):
- User asks about current versions, releases, or "latest" anything
- User asks about recent changes, updates, or deprecations
- User asks about best practices or recommendations (these evolve)
- You're uncertain about specific library APIs, syntax, or features
- User asks to compare tools or libraries
- Troubleshooting errors or issues that may have known solutions
- You just installed a new library - ALWAYS search for its docs before using it
- Any question where your training data might be outdated

DO NOT wait for the user to explicitly ask you to search - if the question benefits from current information, search first.

Usage:
- Provide a search query to find relevant web pages
- Optionally filter results by allowed or blocked domains
- Returns titles, links, and snippets for each result

Parameters:
- query: The search query (required, min 2 characters)
- allowed_domains: Only include results from these domains
- blocked_domains: Exclude results from these domains

Examples:
- Basic search: query="react hooks tutorial"
- Filter by domain: query="typescript generics", allowed_domains=["developer.mozilla.org", "typescriptlang.org"]
- Exclude domains: query="python async", blocked_domains=["w3schools.com"]
`,
    category: ToolCategory.GENERAL,
    defaultPermission: PermissionLevel.ALLOW,
    readOnly: true,
    inputSchema: z.object({
        query: z.string().min(2).describe("The search query"),
        allowed_domains: z
            .array(z.string())
            .optional()
            .describe("Only include results from these domains"),
        blocked_domains: z
            .array(z.string())
            .optional()
            .describe("Exclude results from these domains"),
    }),
    execute: async ({ query, allowed_domains, blocked_domains }, options) => {
        try {
            // Use DuckDuckGo HTML search (works without JavaScript)
            const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
            // Fetch search results
            const response = await fetch(searchUrl, {
                headers: {
                    ...BROWSER_HEADERS,
                    Referer: "https://html.duckduckgo.com/",
                },
                redirect: "follow",
                signal: options?.abortSignal,
            });
            if (!response.ok) {
                throw new Error(`Search returned status ${response.status}`);
            }
            const html = await response.text();
            // Parse results
            let results = parseDuckDuckGoResults(html);
            // Filter by domains
            results = filterByDomains(results, allowed_domains, blocked_domains);
            return { results };
        }
        catch (error) {
            getLogger().error("Web search failed", getErrorMeta({ query, error }));
            if (error instanceof Error && error.name === "AbortError") {
                throw new Error("Web search was aborted");
            }
            if (error instanceof Error) {
                throw new Error(`Failed to search: ${error.message}`);
            }
            throw new Error("Failed to search: Unknown error");
        }
    },
    getSummary: (input, output) => ({
        input: `query: "${input.query}"`,
        output: `Found ${output.results.length} result${output.results.length === 1 ? "" : "s"}`,
    }),
}));
//# sourceMappingURL=web-search.js.map