/**
 * WebSearchService —— 外部公开信息搜索（区别于 knowledge_query 的内部知识库）。
 *
 * Provider 选择（auto-detect，参考 OpenClaw 模式）：
 *   1. 优先 Tavily：TAVILY_API_KEY 配置时启用，稳定 + 引用准确（生产推荐）
 *   2. 兜底 DuckDuckGo HTML 抓取：无 key 直接用，dev / demo 体验零成本
 *      （HTML 抓取 brittle，生产仍建议 Tavily）
 *
 * 设计意图：让 LLM 在没有内部数据时也能查"外面世界"——FF91、Freddie Future
 * 融资、行业新闻、新模型发布等。
 */

import { Injectable, Logger } from '@nestjs/common';
import { stripHtmlTags, decodeHtmlEntities } from '../utils/html.util';

export interface WebSearchResult {
  url: string;
  title: string;
  snippet: string;
}

export interface WebSearchOutput {
  query: string;
  provider: 'tavily' | 'duckduckgo';
  results: readonly WebSearchResult[];
}

@Injectable()
export class WebSearchService {
  private readonly logger = new Logger(WebSearchService.name);

  /**
   * 执行搜索。Tavily 优先；无 key 落 DuckDuckGo HTML 兜底。
   * 都失败时抛 error（调用方决定返回给 LLM）。
   */
  async search(query: string, limit = 5): Promise<WebSearchOutput> {
    const q = query.trim();
    if (!q) throw new Error('query required');

    const tavilyKey = process.env.TAVILY_API_KEY;
    if (tavilyKey) {
      try {
        const results = await this.searchTavily(q, tavilyKey, limit);
        return { query: q, provider: 'tavily', results };
      } catch (err) {
        this.logger.warn(`Tavily failed, falling back to DuckDuckGo: ${(err as Error).message}`);
      }
    }

    const results = await this.searchDuckDuckGo(q, limit);
    return { query: q, provider: 'duckduckgo', results };
  }

  private async searchTavily(
    query: string,
    apiKey: string,
    limit: number,
  ): Promise<WebSearchResult[]> {
    const res = await fetch('https://api.tavily.com/search', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        api_key: apiKey,
        query,
        max_results: limit,
        search_depth: 'basic',
      }),
      signal: AbortSignal.timeout(12000),
    });
    if (!res.ok) {
      throw new Error(`Tavily HTTP ${res.status}`);
    }
    const data = (await res.json()) as { results?: Array<{ url: string; title: string; content: string }> };
    return (data.results ?? []).slice(0, limit).map((r) => ({
      url: r.url,
      title: r.title,
      snippet: (r.content ?? '').slice(0, 300),
    }));
  }

  /**
   * DuckDuckGo HTML 抓取兜底 —— 无 key，dev / demo 直接用。
   * 解析 https://html.duckduckgo.com/html/?q=xxx 的 result__a / result__snippet。
   * DDG HTML 嵌套层多，正则按 block 抽容易匹配不上；改成**全局抽两个数组按序配对**。
   * Brittle（DDG 改 HTML 就坏），生产应换 Tavily / Brave / Bing。
   */
  private async searchDuckDuckGo(query: string, limit: number): Promise<WebSearchResult[]> {
    const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
    const res = await fetch(url, {
      headers: {
        'User-Agent':
          'Mozilla/5.0 (compatible; FFAI-Agent/1.0; +https://ff.com/ai)',
      },
      signal: AbortSignal.timeout(12000),
    });
    if (!res.ok) {
      throw new Error(`DuckDuckGo HTTP ${res.status}`);
    }
    const html = await res.text();

    // 抽 result__a（标题 + URL） — 一条对应一个 result
    const titleRe = /<a[^>]+class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g;
    // 抽 result__snippet（描述）— DDG 用 <a> 而不是 <div>
    const snippetRe = /<a[^>]+class="result__snippet"[^>]*>([\s\S]*?)<\/a>/g;

    const titles: Array<{ url: string; title: string }> = [];
    let m: RegExpExecArray | null;
    while ((m = titleRe.exec(html)) !== null) {
      titles.push({
        url: decodeDDGRedirect(m[1]),
        title: cleanHtml(m[2]),
      });
    }
    const snippets: string[] = [];
    while ((m = snippetRe.exec(html)) !== null) {
      snippets.push(cleanHtml(m[1]).slice(0, 300));
    }

    const results: WebSearchResult[] = [];
    for (let i = 0; i < titles.length && results.length < limit; i++) {
      if (!titles[i].url || !titles[i].title) continue;
      results.push({
        url: titles[i].url,
        title: titles[i].title,
        snippet: snippets[i] ?? '',
      });
    }
    return results;
  }
}

function cleanHtml(s: string): string {
  return decodeHtmlEntities(stripHtmlTags(s)).trim();
}

/**
 * DDG HTML 把外链包成 `//duckduckgo.com/l/?uddg=<url-encoded>&rut=...`，
 * 提取真实 URL；若已经是 http(s):// 则直接返回。
 */
function decodeDDGRedirect(href: string): string {
  if (href.startsWith('http')) return href;
  const m = href.match(/[?&]uddg=([^&]+)/);
  if (m) {
    try {
      return decodeURIComponent(m[1]);
    } catch {
      // 解码失败 fallthrough
    }
  }
  if (href.startsWith('//')) return `https:${href}`;
  return href;
}
