"""WebSearchTool — 通用网页搜索工具。

支持多种搜索后端，按优先级自动降级：
1. Tavily API（需要 API key，质量最好）
2. Serper API（需要 API key，Google 搜索结果）
3. DuckDuckGo Lite（免费，无需 API key，降级方案）
"""

import json
import logging
import re
import urllib.parse
from typing import Any

import httpx

from agentkit.tools.base import Tool

logger = logging.getLogger(__name__)


class WebSearchTool(Tool):
    """通用网页搜索工具。

    支持三种后端，按优先级降级：
    - Tavily API（高质量，需 key）
    - Serper API（Google 结果，需 key）
    - DuckDuckGo Lite（免费降级方案）
    """

    def __init__(
        self,
        name: str = "web_search",
        description: str = "搜索互联网信息。返回搜索结果列表，包含标题、链接和摘要。",
        input_schema: dict[str, Any] | None = None,
        output_schema: dict[str, Any] | None = None,
        version: str = "1.0.0",
        tags: list[str] | None = None,
        tavily_api_key: str | None = None,
        serper_api_key: str | None = None,
        default_max_results: int = 5,
    ):
        super().__init__(
            name=name,
            description=description,
            input_schema=input_schema or self._default_input_schema(),
            output_schema=output_schema or self._default_output_schema(),
            version=version,
            tags=tags or ["search", "web"],
        )
        self._tavily_key = tavily_api_key
        self._serper_key = serper_api_key
        self._default_max_results = default_max_results

    @staticmethod
    def _default_input_schema() -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "搜索关键词",
                },
                "max_results": {
                    "type": "integer",
                    "description": "最大返回结果数（默认 5）",
                    "default": 5,
                },
            },
            "required": ["query"],
        }

    @staticmethod
    def _default_output_schema() -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "results": {
                    "type": "array",
                    "items": {
                        "type": "object",
                        "properties": {
                            "title": {"type": "string"},
                            "url": {"type": "string"},
                            "snippet": {"type": "string"},
                        },
                    },
                    "description": "搜索结果列表",
                },
                "total": {"type": "integer", "description": "结果总数"},
                "backend": {"type": "string", "description": "使用的搜索后端"},
                "success": {"type": "boolean", "description": "是否成功"},
                "error": {"type": "string", "description": "错误信息（仅失败时）"},
            },
        }

    async def execute(self, **kwargs) -> dict:
        """执行网页搜索。"""
        query = kwargs.get("query")
        if not query:
            return {"error": "query 参数是必需的", "results": [], "total": 0, "success": False}

        max_results = kwargs.get("max_results", self._default_max_results)

        # Try backends in priority order
        if self._tavily_key:
            result = await self._search_tavily(query, max_results)
            if result.get("success"):
                return result
            logger.warning(f"Tavily search failed, falling back: {result.get('error')}")

        if self._serper_key:
            result = await self._search_serper(query, max_results)
            if result.get("success"):
                return result
            logger.warning(f"Serper search failed, falling back: {result.get('error')}")

        # Fallback: DuckDuckGo
        return await self._search_duckduckgo(query, max_results)

    async def _search_tavily(self, query: str, max_results: int) -> dict:
        """Tavily API search."""
        try:
            async with httpx.AsyncClient(timeout=15) as client:
                resp = await client.post(
                    "https://api.tavily.com/search",
                    json={
                        "api_key": self._tavily_key,
                        "query": query,
                        "max_results": max_results,
                        "search_depth": "basic",
                    },
                )
                resp.raise_for_status()
                data = resp.json()

            results = []
            for item in data.get("results", [])[:max_results]:
                results.append({
                    "title": item.get("title", ""),
                    "url": item.get("url", ""),
                    "snippet": item.get("content", "")[:300],
                })

            return {"results": results, "total": len(results), "backend": "tavily", "success": True}

        except Exception as e:
            logger.error(f"Tavily search error: {e}")
            return {"error": str(e), "results": [], "total": 0, "success": False}

    async def _search_serper(self, query: str, max_results: int) -> dict:
        """Serper API (Google) search."""
        try:
            async with httpx.AsyncClient(timeout=15) as client:
                resp = await client.post(
                    "https://google.serper.dev/search",
                    json={"q": query, "num": max_results},
                    headers={"X-API-KEY": self._serper_key},
                )
                resp.raise_for_status()
                data = resp.json()

            results = []
            for item in data.get("organic", [])[:max_results]:
                results.append({
                    "title": item.get("title", ""),
                    "url": item.get("link", ""),
                    "snippet": item.get("snippet", "")[:300],
                })

            return {"results": results, "total": len(results), "backend": "serper", "success": True}

        except Exception as e:
            logger.error(f"Serper search error: {e}")
            return {"error": str(e), "results": [], "total": 0, "success": False}

    async def _search_duckduckgo(self, query: str, max_results: int) -> dict:
        """DuckDuckGo search (free, no API key needed).

        Strategy:
        1. Try HTML search (may be blocked by anti-bot)
        2. Try Instant Answer API with original query
        3. Try Instant Answer API with translated English query (for Chinese queries)
        4. Try Bing search as final fallback
        """
        try:
            # Try HTML search first (more results when available)
            result = await self._search_duckduckgo_html(query, max_results)
            if result.get("success") and result.get("total", 0) > 0:
                return result

            # Try Instant Answer API with original query
            result = await self._search_duckduckgo_instant(query, max_results)
            if result.get("success") and result.get("total", 0) > 0:
                return result

            # For Chinese queries, try translating key terms to English
            if self._contains_cjk(query):
                english_query = self._cjk_to_english_hint(query)
                if english_query != query:
                    logger.info(f"Retrying DuckDuckGo with English query: {english_query}")
                    result = await self._search_duckduckgo_instant(english_query, max_results)
                    if result.get("success") and result.get("total", 0) > 0:
                        return result

            # Final fallback: try Bing search
            result = await self._search_bing(query, max_results)
            if result.get("success") and result.get("total", 0) > 0:
                return result

            # Return whatever we have (may be empty)
            return result

        except Exception as e:
            logger.error(f"DuckDuckGo search error: {e}")
            return {
                "error": f"Search unavailable: {e}",
                "results": [],
                "total": 0,
                "backend": "duckduckgo",
                "success": False,
            }

    @staticmethod
    def _contains_cjk(text: str) -> bool:
        """Check if text contains CJK characters."""
        for ch in text:
            if '\u4e00' <= ch <= '\u9fff' or '\u3040' <= ch <= '\u309f' or '\u30a0' <= ch <= '\u30ff':
                return True
        return False

    @staticmethod
    def _cjk_to_english_hint(query: str) -> str:
        """Simple CJK-to-English keyword mapping for better DuckDuckGo results."""
        # Common Chinese query patterns to English
        mappings = {
            "是什么": "definition meaning",
            "什么意思": "meaning definition",
            "怎么": "how to",
            "为什么": "why",
            "如何": "how to",
            "搜索": "",
            "查一下": "",
            "帮我": "",
            "请": "",
        }
        result = query
        for cn, en in mappings.items():
            result = result.replace(cn, f" {en} ")
        # Remove extra spaces
        result = " ".join(result.split())
        return result if result.strip() else query

    async def _search_duckduckgo_html(self, query: str, max_results: int) -> dict:
        """DuckDuckGo HTML search with robust parsing."""
        try:
            encoded_query = urllib.parse.quote(query)
            url = f"https://html.duckduckgo.com/html/?q={encoded_query}"

            async with httpx.AsyncClient(timeout=15, follow_redirects=True) as client:
                resp = await client.get(
                    url,
                    headers={
                        "User-Agent": (
                            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
                            "AppleWebKit/537.36 (KHTML, like Gecko) "
                            "Chrome/120.0.0.0 Safari/537.36"
                        ),
                    },
                )
                html = resp.text

            results = self._parse_duckduckgo_html(html, max_results)

            # If no results from standard parsing, try alternative patterns
            if not results:
                results = self._parse_duckduckgo_html_alt(html, max_results)

            return {"results": results, "total": len(results), "backend": "duckduckgo", "success": True}

        except Exception as e:
            logger.error(f"DuckDuckGo HTML search error: {e}")
            return {"error": str(e), "results": [], "total": 0, "backend": "duckduckgo", "success": False}

    async def _search_duckduckgo_instant(self, query: str, max_results: int) -> dict:
        """DuckDuckGo Instant Answer API — returns abstract/related topics."""
        try:
            encoded_query = urllib.parse.quote(query)
            url = f"https://api.duckduckgo.com/?q={encoded_query}&format=json&no_html=1&skip_disambig=0"

            async with httpx.AsyncClient(timeout=10) as client:
                resp = await client.get(url)
                resp.raise_for_status()
                data = resp.json()

            results = []

            # Abstract (direct answer)
            abstract = data.get("Abstract")
            if abstract:
                results.append({
                    "title": data.get("Heading", query),
                    "url": data.get("AbstractURL", ""),
                    "snippet": abstract[:300],
                })

            # Related topics
            for topic in data.get("RelatedTopics", [])[:max_results]:
                if len(results) >= max_results:
                    break
                if isinstance(topic, dict) and "Text" in topic:
                    results.append({
                        "title": topic.get("Text", "")[:80],
                        "url": topic.get("FirstURL", ""),
                        "snippet": topic.get("Text", "")[:300],
                    })

            # Infobox
            infobox = data.get("Infobox")
            if infobox and isinstance(infobox, dict):
                content = infobox.get("content", [])
                for item in content[:2]:
                    if len(results) >= max_results:
                        break
                    if isinstance(item, dict) and item.get("value"):
                        results.append({
                            "title": item.get("label", ""),
                            "url": "",
                            "snippet": str(item.get("value", ""))[:300],
                        })

            return {"results": results, "total": len(results), "backend": "duckduckgo_instant", "success": True}

        except Exception as e:
            logger.error(f"DuckDuckGo Instant API error: {e}")
            return {"error": str(e), "results": [], "total": 0, "backend": "duckduckgo_instant", "success": False}

    async def _search_bing(self, query: str, max_results: int) -> dict:
        """Bing search as a reliable fallback (free, no API key needed).

        Uses Bing's search page with proper headers to avoid blocking.
        """
        try:
            encoded_query = urllib.parse.quote(query)
            url = f"https://www.bing.com/search?q={encoded_query}&count={max_results}"

            async with httpx.AsyncClient(timeout=15, follow_redirects=True) as client:
                resp = await client.get(
                    url,
                    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 Edg/131.0.0.0"
                        ),
                        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
                        "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
                    },
                )
                html = resp.text

            results = self._parse_bing_html(html, max_results)
            return {"results": results, "total": len(results), "backend": "bing", "success": True}

        except Exception as e:
            logger.error(f"Bing search error: {e}")
            return {"error": str(e), "results": [], "total": 0, "backend": "bing", "success": False}

    @staticmethod
    def _parse_bing_html(html: str, max_results: int) -> list[dict[str, str]]:
        """Parse Bing search results HTML."""
        results: list[dict[str, str]] = []

        # Bing uses <li class="b_algo"> for organic results
        # Title: <h2><a href="...">title</a></h2>
        # Snippet: <p class="b_lineclamp2"> or <div class="b_caption"><p>
        algo_pattern = re.compile(
            r'<li[^>]*class="b_algo"[^>]*>(.*?)</li>',
            re.DOTALL,
        )
        link_pattern = re.compile(
            r'<h2[^>]*>\s*<a[^>]*href="([^"]*)"[^>]*>(.*?)</a>',
            re.DOTALL,
        )
        snippet_pattern = re.compile(
            r'<p[^>]*>(.*?)</p>',
            re.DOTALL,
        )

        for algo_match in algo_pattern.finditer(html):
            if len(results) >= max_results:
                break
            block = algo_match.group(1)

            link_match = link_pattern.search(block)
            if not link_match:
                continue

            url = link_match.group(1)
            title = re.sub(r"<[^>]+>", "", link_match.group(2)).strip()

            if not title or not url.startswith("http"):
                continue

            snippet = ""
            snippet_match = snippet_pattern.search(block[link_match.end():])
            if snippet_match:
                snippet = re.sub(r"<[^>]+>", "", snippet_match.group(1)).strip()

            results.append({
                "title": title[:200],
                "url": url,
                "snippet": snippet[:300],
            })

        return results

    @staticmethod
    def _parse_duckduckgo_html(html: str, max_results: int) -> list[dict[str, str]]:
        """Parse DuckDuckGo HTML search results."""
        results: list[dict[str, str]] = []

        # Pattern for html.duckduckgo.com: <a class="result__a" href="...">title</a>
        link_pattern = re.compile(
            r'<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>(.*?)</a>',
            re.DOTALL,
        )
        snippet_pattern = re.compile(
            r'<a[^>]*class="result__snippet"[^>]*>(.*?)</a>',
            re.DOTALL,
        )

        links = list(link_pattern.finditer(html))
        snippets = list(snippet_pattern.finditer(html))

        for i, match in enumerate(links):
            if len(results) >= max_results:
                break

            url = match.group(1)
            title = re.sub(r"<[^>]+>", "", match.group(2)).strip()

            # Skip ad/tracking links
            if not url.startswith("http") or "duckduckgo.com" in url:
                continue

            snippet = ""
            if i < len(snippets):
                snippet = re.sub(r"<[^>]+>", "", snippets[i].group(1)).strip()

            results.append({
                "title": title[:200],
                "url": url,
                "snippet": snippet[:300],
            })

        return results

    @staticmethod
    def _parse_duckduckgo_html_alt(html: str, max_results: int) -> list[dict[str, str]]:
        """Alternative DuckDuckGo HTML parser for lite/html variants."""
        results: list[dict[str, str]] = []

        # Pattern for lite.duckduckgo.com
        link_pattern = re.compile(
            r'<a[^>]*class="result-link"[^>]*href="([^"]*)"[^>]*>(.*?)</a>',
            re.DOTALL,
        )
        snippet_pattern = re.compile(
            r'<td[^>]*class="result-snippet"[^>]*>(.*?)</td>',
            re.DOTALL,
        )

        links = list(link_pattern.finditer(html))
        snippets = list(snippet_pattern.finditer(html))

        for i, match in enumerate(links):
            if len(results) >= max_results:
                break

            url = match.group(1)
            title = re.sub(r"<[^>]+>", "", match.group(2)).strip()

            if not url.startswith("http") or "duckduckgo.com" in url:
                continue

            snippet = ""
            if i < len(snippets):
                snippet = re.sub(r"<[^>]+>", "", snippets[i].group(1)).strip()

            results.append({
                "title": title[:200],
                "url": url,
                "snippet": snippet[:300],
            })

        # If still no results, try generic <a> with href containing external URLs
        if not results:
            generic_pattern = re.compile(
                r'<a[^>]*href="(https?://(?!duckduckgo\.com)[^"]*)"[^>]*>(.*?)</a>',
                re.DOTALL,
            )
            for match in generic_pattern.finditer(html):
                if len(results) >= max_results:
                    break
                url = match.group(1)
                title = re.sub(r"<[^>]+>", "", match.group(2)).strip()
                if title and len(title) > 5:
                    results.append({
                        "title": title[:200],
                        "url": url,
                        "snippet": "",
                    })

        return results
