"""Serper.dev 客户端（Google Search API 代理）。

为什么用 Serper：
- 2500 次免费额度，远超 SerpAPI 的 100/月
- 支持 Google Search、Scholar、News、Images、Maps
- Google Patents 无专用 endpoint，但可用 `site:patents.google.com` 技巧
- 价格比 SerpAPI 便宜 3-5×

用途：
- 专利检索：通用 search + `site:patents.google.com`
- 学术论文：/scholar endpoint
- 新闻：/news endpoint（时效性敏感的行业动态）

httpx 客户端使用 trust_env=False 绕过系统 socks 代理（macOS Clash 会导致 TLS EOF）。
"""

from __future__ import annotations

import os
from dataclasses import dataclass
from typing import Any, Literal

import httpx


SERPER_BASE = "https://google.serper.dev"


@dataclass
class SerperHit:
    title: str
    url: str
    snippet: str
    source: str = ""       # 论文出处 / 新闻媒体
    date: str = ""         # 发表日期（如 scholar / news 返回的话）
    cited_by: int = 0      # 学术论文的引用数（仅 scholar）


class SerperError(RuntimeError):
    pass


class SerperClient:
    def __init__(self, api_key: str | None = None, timeout: float = 30.0) -> None:
        self.api_key = api_key or os.environ.get("SERPAPI_KEY") or os.environ.get("SERPER_API_KEY")
        if not self.api_key:
            raise SerperError("SERPAPI_KEY / SERPER_API_KEY not set")
        self._client = httpx.Client(trust_env=False, timeout=timeout)

    def close(self) -> None:
        self._client.close()

    def __enter__(self) -> "SerperClient":
        return self

    def __exit__(self, *_args: Any) -> None:
        self.close()

    def _post(self, path: str, body: dict) -> dict:
        try:
            r = self._client.post(
                f"{SERPER_BASE}{path}",
                json=body,
                headers={
                    "X-API-KEY": self.api_key,
                    "Content-Type": "application/json",
                },
            )
        except httpx.RequestError as e:
            raise SerperError(f"network error: {e}")
        if r.status_code != 200:
            raise SerperError(f"HTTP {r.status_code}: {r.text[:300]}")
        try:
            return r.json()
        except Exception as e:
            raise SerperError(f"invalid JSON: {e}")

    def search(
        self,
        query: str,
        *,
        num_results: int = 10,
        gl: str = "us",
        hl: str = "en",
    ) -> list[SerperHit]:
        """通用 Google 搜索。支持 site: / filetype: / 引号短语等 Google 高级语法。"""
        data = self._post("/search", {
            "q": query,
            "num": num_results,
            "gl": gl,
            "hl": hl,
        })
        hits: list[SerperHit] = []
        for item in (data.get("organic") or [])[:num_results]:
            hits.append(SerperHit(
                title=(item.get("title") or "")[:200],
                url=item.get("link") or "",
                snippet=(item.get("snippet") or "")[:600],
                date=item.get("date") or "",
            ))
        return hits

    def scholar(
        self,
        query: str,
        *,
        num_results: int = 10,
        year_low: int | None = None,
        year_high: int | None = None,
    ) -> list[SerperHit]:
        """Google Scholar 搜索——学术论文首选。

        返回带引用数、发表年份等元数据，权威信源识别更准确。
        """
        body: dict[str, Any] = {"q": query, "num": num_results}
        if year_low is not None:
            body["tbs"] = f"cdr:1,cd_min:{year_low}" + (f",cd_max:{year_high}" if year_high else "")
        data = self._post("/scholar", body)
        hits: list[SerperHit] = []
        for item in (data.get("organic") or [])[:num_results]:
            hits.append(SerperHit(
                title=(item.get("title") or "")[:200],
                url=item.get("link") or "",
                snippet=(item.get("snippet") or "")[:600],
                source=(item.get("publicationInfo") or "")[:200],
                year=item.get("year") or "",
                cited_by=item.get("citedBy") or 0,
            ) if False else SerperHit(
                title=(item.get("title") or "")[:200],
                url=item.get("link") or "",
                snippet=(item.get("snippet") or "")[:600],
                source=(item.get("publicationInfo") or "")[:200],
                date=str(item.get("year") or ""),
                cited_by=item.get("citedBy") or 0,
            ))
        return hits

    def patents(
        self,
        query: str,
        *,
        num_results: int = 10,
    ) -> list[SerperHit]:
        """Google Patents 检索——用 site: 技巧走通用搜索。

        serper.dev 没有专门的 patents endpoint，但 `site:patents.google.com` 效果很好。
        """
        combined = f"site:patents.google.com {query}"
        return self.search(combined, num_results=num_results)

    def news(
        self,
        query: str,
        *,
        num_results: int = 10,
        time_range: Literal["d", "w", "m", "y"] | None = None,
    ) -> list[SerperHit]:
        """Google News 搜索——时效敏感行业动态。

        time_range: d=24h, w=7d, m=30d, y=1y
        """
        body: dict[str, Any] = {"q": query, "num": num_results}
        if time_range:
            body["tbs"] = f"qdr:{time_range}"
        data = self._post("/news", body)
        hits: list[SerperHit] = []
        for item in (data.get("news") or [])[:num_results]:
            hits.append(SerperHit(
                title=(item.get("title") or "")[:200],
                url=item.get("link") or "",
                snippet=(item.get("snippet") or "")[:600],
                source=(item.get("source") or "")[:200],
                date=item.get("date") or "",
            ))
        return hits


if __name__ == "__main__":
    from scripts.lib.zenmux_client import load_secrets
    load_secrets()
    with SerperClient() as c:
        print("=== Patents: dual-target siRNA ===")
        for h in c.patents("dual-target siRNA GalNAc conjugate", num_results=3):
            print(f"  {h.title[:70]}")
            print(f"    {h.url}")
        print("\n=== Scholar: dual-target RNAi ===")
        for h in c.scholar("dual-target RNAi drug 2024", num_results=3):
            print(f"  {h.title[:70]}  [引用 {h.cited_by}] ({h.date})")
            print(f"    {h.url}")
            print(f"    源: {h.source[:80]}")
