import { Type } from "@sinclair/typebox";
import { formatCliCommand } from "../../cli/command-format.js";
import { wrapWebContent } from "../../security/external-content.js";
import { jsonResult, readNumberParam, readStringParam } from "./common.js";
import { DEFAULT_CACHE_TTL_MINUTES, DEFAULT_TIMEOUT_SECONDS, normalizeCacheKey, readCache, readResponseText, resolveCacheTtlMs, resolveTimeoutSeconds, withTimeout, writeCache, } from "./web-shared.js";
const SEARCH_PROVIDERS = ["brave", "perplexity"];
const DEFAULT_SEARCH_COUNT = 5;
const MAX_SEARCH_COUNT = 10;
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 PERPLEXITY_KEY_PREFIXES = ["pplx-"];
const OPENROUTER_KEY_PREFIXES = ["sk-or-"];
const SEARCH_CACHE = new Map();
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})$/;
// Brave Search API requires specific subtags for certain languages.
// e.g. "zh" is rejected (422); must be "zh-hans" or "zh-hant".
const BRAVE_LANG_ALIASES = {
    zh: "zh-hans",
    "zh-cn": "zh-hans",
    "zh-sg": "zh-hans",
    "zh-tw": "zh-hant",
    "zh-hk": "zh-hant",
};
const WebSearchSchema = Type.Object({
    query: Type.String({ description: "Search query string." }),
    count: Type.Optional(Type.Number({
        description: "Number of results to return (1-10).",
        minimum: 1,
        maximum: MAX_SEARCH_COUNT,
    })),
    country: Type.Optional(Type.String({
        description: "2-letter country code for region-specific results (e.g., 'DE', 'US', 'ALL'). Default: 'US'.",
    })),
    search_lang: Type.Optional(Type.String({
        description: "ISO language code for search results (e.g., 'de', 'en', 'fr'). For Chinese use 'zh-hans' (Simplified) or 'zh-hant' (Traditional); bare 'zh' is auto-mapped to 'zh-hans'.",
    })),
    ui_lang: Type.Optional(Type.String({
        description: "ISO language code for UI elements.",
    })),
    freshness: Type.Optional(Type.String({
        description: "Filter results by discovery time (Brave only). Values: 'pd' (past 24h), 'pw' (past week), 'pm' (past month), 'py' (past year), or date range 'YYYY-MM-DDtoYYYY-MM-DD'.",
    })),
});
function resolveSearchConfig(cfg) {
    const search = cfg?.tools?.web?.search;
    if (!search || typeof search !== "object")
        return undefined;
    return search;
}
function resolveSearchEnabled(params) {
    if (typeof params.search?.enabled === "boolean")
        return params.search.enabled;
    if (params.sandboxed)
        return true;
    return true;
}
function resolveSearchApiKey(search) {
    const fromConfig = search && "apiKey" in search && typeof search.apiKey === "string" ? search.apiKey.trim() : "";
    const fromEnv = (process.env.BRAVE_API_KEY ?? "").trim();
    return fromConfig || fromEnv || undefined;
}
function missingSearchKeyPayload(provider) {
    if (provider === "perplexity") {
        return {
            error: "missing_perplexity_api_key",
            message: "web_search (perplexity) needs an API key. Set PERPLEXITY_API_KEY or OPENROUTER_API_KEY in the Gateway environment, or configure tools.web.search.perplexity.apiKey.",
            docs: "https://docs.clawd.bot/tools/web",
        };
    }
    return {
        error: "missing_brave_api_key",
        message: `web_search needs a Brave Search API key. Run \`${formatCliCommand("openclaw-cn configure --section web")}\` to store it, or set BRAVE_API_KEY in the Gateway environment.`,
        docs: "https://docs.clawd.bot/tools/web",
    };
}
function resolveSearchProvider(search) {
    const raw = search && "provider" in search && typeof search.provider === "string"
        ? search.provider.trim().toLowerCase()
        : "";
    if (raw === "perplexity")
        return "perplexity";
    if (raw === "brave")
        return "brave";
    return "brave";
}
function resolvePerplexityConfig(search) {
    if (!search || typeof search !== "object")
        return {};
    const perplexity = "perplexity" in search ? search.perplexity : undefined;
    if (!perplexity || typeof perplexity !== "object")
        return {};
    return perplexity;
}
function resolvePerplexityApiKey(perplexity) {
    const fromConfig = normalizeApiKey(perplexity?.apiKey);
    if (fromConfig) {
        return { apiKey: fromConfig, source: "config" };
    }
    const fromEnvPerplexity = normalizeApiKey(process.env.PERPLEXITY_API_KEY);
    if (fromEnvPerplexity) {
        return { apiKey: fromEnvPerplexity, source: "perplexity_env" };
    }
    const fromEnvOpenRouter = normalizeApiKey(process.env.OPENROUTER_API_KEY);
    if (fromEnvOpenRouter) {
        return { apiKey: fromEnvOpenRouter, source: "openrouter_env" };
    }
    return { apiKey: undefined, source: "none" };
}
function normalizeApiKey(key) {
    return typeof key === "string" ? key.trim() : "";
}
function inferPerplexityBaseUrlFromApiKey(apiKey) {
    if (!apiKey)
        return undefined;
    const normalized = apiKey.toLowerCase();
    if (PERPLEXITY_KEY_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {
        return "direct";
    }
    if (OPENROUTER_KEY_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {
        return "openrouter";
    }
    return undefined;
}
function resolvePerplexityBaseUrl(perplexity, apiKeySource = "none", apiKey) {
    const fromConfig = perplexity && "baseUrl" in perplexity && typeof perplexity.baseUrl === "string"
        ? perplexity.baseUrl.trim()
        : "";
    if (fromConfig)
        return fromConfig;
    if (apiKeySource === "perplexity_env")
        return PERPLEXITY_DIRECT_BASE_URL;
    if (apiKeySource === "openrouter_env")
        return DEFAULT_PERPLEXITY_BASE_URL;
    if (apiKeySource === "config") {
        const inferred = inferPerplexityBaseUrlFromApiKey(apiKey);
        if (inferred === "direct")
            return PERPLEXITY_DIRECT_BASE_URL;
        if (inferred === "openrouter")
            return DEFAULT_PERPLEXITY_BASE_URL;
    }
    return DEFAULT_PERPLEXITY_BASE_URL;
}
function resolvePerplexityModel(perplexity) {
    const fromConfig = perplexity && "model" in perplexity && typeof perplexity.model === "string"
        ? perplexity.model.trim()
        : "";
    return fromConfig || DEFAULT_PERPLEXITY_MODEL;
}
function resolveSearchCount(value, fallback) {
    const parsed = typeof value === "number" && Number.isFinite(value) ? value : fallback;
    const clamped = Math.max(1, Math.min(MAX_SEARCH_COUNT, Math.floor(parsed)));
    return clamped;
}
function normalizeFreshness(value) {
    if (!value)
        return undefined;
    const trimmed = value.trim();
    if (!trimmed)
        return undefined;
    const lower = trimmed.toLowerCase();
    if (BRAVE_FRESHNESS_SHORTCUTS.has(lower))
        return lower;
    const match = trimmed.match(BRAVE_FRESHNESS_RANGE);
    if (!match)
        return undefined;
    const [, start, end] = match;
    if (!isValidIsoDate(start) || !isValidIsoDate(end))
        return undefined;
    if (start > end)
        return undefined;
    return `${start}to${end}`;
}
/** Normalize language codes to Brave-accepted values (e.g. "zh" → "zh-hans"). */
function normalizeBraveLang(lang) {
    if (!lang)
        return undefined;
    const lower = lang.trim().toLowerCase();
    if (!lower)
        return undefined;
    return BRAVE_LANG_ALIASES[lower] ?? lower;
}
function isValidIsoDate(value) {
    if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
        return false;
    const [year, month, day] = value.split("-").map((part) => Number.parseInt(part, 10));
    if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day))
        return false;
    const date = new Date(Date.UTC(year, month - 1, day));
    return (date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day);
}
function resolveSiteName(url) {
    if (!url)
        return undefined;
    try {
        return new URL(url).hostname;
    }
    catch {
        return undefined;
    }
}
async function runPerplexitySearch(params) {
    const endpoint = `${params.baseUrl.replace(/\/$/, "")}/chat/completions`;
    const res = await fetch(endpoint, {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
            Authorization: `Bearer ${params.apiKey}`,
            "HTTP-Referer": "https://clawdbot.com",
            "X-Title": "Clawdbot Web Search",
        },
        body: JSON.stringify({
            model: params.model,
            messages: [
                {
                    role: "user",
                    content: params.query,
                },
            ],
        }),
        signal: withTimeout(undefined, params.timeoutSeconds * 1000),
    });
    if (!res.ok) {
        const detail = await readResponseText(res);
        throw new Error(`Perplexity API error (${res.status}): ${detail || res.statusText}`);
    }
    const data = (await res.json());
    const content = data.choices?.[0]?.message?.content ?? "No response";
    const citations = data.citations ?? [];
    return { content, citations };
}
async function runWebSearch(params) {
    const cacheKey = normalizeCacheKey(params.provider === "brave"
        ? `${params.provider}:${params.query}:${params.count}:${params.country || "default"}:${params.search_lang || "default"}:${params.ui_lang || "default"}:${params.freshness || "default"}`
        : `${params.provider}:${params.query}:${params.count}:${params.country || "default"}:${params.search_lang || "default"}:${params.ui_lang || "default"}`);
    const cached = readCache(SEARCH_CACHE, cacheKey);
    if (cached)
        return { ...cached.value, cached: true };
    const start = Date.now();
    if (params.provider === "perplexity") {
        const { content, citations } = await runPerplexitySearch({
            query: params.query,
            apiKey: params.apiKey,
            baseUrl: params.perplexityBaseUrl ?? DEFAULT_PERPLEXITY_BASE_URL,
            model: params.perplexityModel ?? DEFAULT_PERPLEXITY_MODEL,
            timeoutSeconds: params.timeoutSeconds,
        });
        const payload = {
            query: params.query,
            provider: params.provider,
            model: params.perplexityModel ?? DEFAULT_PERPLEXITY_MODEL,
            tookMs: Date.now() - start,
            externalContent: {
                untrusted: true,
                source: "web_search",
                provider: params.provider,
                wrapped: true,
            },
            content: wrapWebContent(content),
            citations,
        };
        writeCache(SEARCH_CACHE, cacheKey, payload, params.cacheTtlMs);
        return payload;
    }
    if (params.provider !== "brave") {
        throw new Error("Unsupported web search provider.");
    }
    const url = new URL(BRAVE_SEARCH_ENDPOINT);
    url.searchParams.set("q", params.query);
    url.searchParams.set("count", String(params.count));
    if (params.country) {
        url.searchParams.set("country", params.country);
    }
    if (params.search_lang) {
        url.searchParams.set("search_lang", normalizeBraveLang(params.search_lang) ?? params.search_lang);
    }
    if (params.ui_lang) {
        url.searchParams.set("ui_lang", normalizeBraveLang(params.ui_lang) ?? params.ui_lang);
    }
    if (params.freshness) {
        url.searchParams.set("freshness", params.freshness);
    }
    const res = await fetch(url.toString(), {
        method: "GET",
        headers: {
            Accept: "application/json",
            "X-Subscription-Token": params.apiKey,
        },
        signal: withTimeout(undefined, params.timeoutSeconds * 1000),
    });
    if (!res.ok) {
        const detail = await readResponseText(res);
        throw new Error(`Brave Search API error (${res.status}): ${detail || res.statusText}`);
    }
    const data = (await res.json());
    const results = Array.isArray(data.web?.results) ? (data.web?.results ?? []) : [];
    const mapped = results.map((entry) => ({
        title: entry.title ?? "",
        url: entry.url ?? "",
        description: entry.description ?? "",
        published: entry.age ?? undefined,
        siteName: resolveSiteName(entry.url ?? ""),
    }));
    const payload = {
        query: params.query,
        provider: params.provider,
        count: mapped.length,
        tookMs: Date.now() - start,
        externalContent: {
            untrusted: true,
            source: "web_search",
            provider: params.provider,
            wrapped: true,
        },
        results: mapped,
    };
    writeCache(SEARCH_CACHE, cacheKey, payload, params.cacheTtlMs);
    return payload;
}
export function createWebSearchTool(options) {
    const search = resolveSearchConfig(options?.config);
    if (!resolveSearchEnabled({ search, sandboxed: options?.sandboxed }))
        return null;
    const provider = resolveSearchProvider(search);
    const perplexityConfig = resolvePerplexityConfig(search);
    const description = provider === "perplexity"
        ? "Search the web using Perplexity Sonar (direct or via OpenRouter). Returns AI-synthesized answers with citations from real-time web search."
        : "Search the web using Brave Search API. Supports region-specific and localized search via country and language parameters. Returns titles, URLs, and snippets for fast research.";
    return {
        label: "Web Search",
        name: "web_search",
        description,
        parameters: WebSearchSchema,
        execute: async (_toolCallId, args) => {
            const perplexityAuth = provider === "perplexity" ? resolvePerplexityApiKey(perplexityConfig) : undefined;
            const apiKey = provider === "perplexity" ? perplexityAuth?.apiKey : resolveSearchApiKey(search);
            if (!apiKey) {
                return jsonResult(missingSearchKeyPayload(provider));
            }
            const params = args;
            const query = readStringParam(params, "query", { required: true });
            const count = readNumberParam(params, "count", { integer: true }) ?? search?.maxResults ?? undefined;
            const country = readStringParam(params, "country");
            const search_lang = readStringParam(params, "search_lang");
            const ui_lang = readStringParam(params, "ui_lang");
            const rawFreshness = readStringParam(params, "freshness");
            if (rawFreshness && provider !== "brave") {
                return jsonResult({
                    error: "unsupported_freshness",
                    message: "freshness is only supported by the Brave web_search provider.",
                    docs: "https://docs.clawd.bot/tools/web",
                });
            }
            const freshness = rawFreshness ? normalizeFreshness(rawFreshness) : undefined;
            if (rawFreshness && !freshness) {
                return jsonResult({
                    error: "invalid_freshness",
                    message: "freshness must be one of pd, pw, pm, py, or a range like YYYY-MM-DDtoYYYY-MM-DD.",
                    docs: "https://docs.clawd.bot/tools/web",
                });
            }
            const result = await runWebSearch({
                query,
                count: resolveSearchCount(count, DEFAULT_SEARCH_COUNT),
                apiKey,
                timeoutSeconds: resolveTimeoutSeconds(search?.timeoutSeconds, DEFAULT_TIMEOUT_SECONDS),
                cacheTtlMs: resolveCacheTtlMs(search?.cacheTtlMinutes, DEFAULT_CACHE_TTL_MINUTES),
                provider,
                country,
                search_lang,
                ui_lang,
                freshness,
                perplexityBaseUrl: resolvePerplexityBaseUrl(perplexityConfig, perplexityAuth?.source, perplexityAuth?.apiKey),
                perplexityModel: resolvePerplexityModel(perplexityConfig),
            });
            return jsonResult(result);
        },
    };
}
export const __testing = {
    inferPerplexityBaseUrlFromApiKey,
    resolvePerplexityBaseUrl,
    normalizeFreshness,
    normalizeBraveLang,
};
