"""Google Trends 数据源（可插拔实现）。

封装 pytrends + 官方 RSS，带本地缓存、指数退避重试、urllib3 兼容补丁。
- gt_trending：国家实时趋势榜（RSS，稳定）
- gt_style：按国家风格种子词抓 related_queries（设计灵感）
- gt_related：按 POD 行业种子词抓 related_queries（行业交叉验证）

注意：related_queries 是「单关键词」接口，一次传多个词会触发 Google /sorry(429)，
因此逐词串行 + 节流 + 快速失败。缓存按 (key, 日期) 分文件：不删除历史文件，
24h 内读最新；超过 24h 重新抓取写当日新文件；抓取失败回退最新历史缓存兜底。
"""
import datetime
import hashlib
import json
import re
import time
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Any, Dict, List

import requests
import urllib3
from urllib3.util.retry import Retry as _Retry

# pytrends 4.x 仍用 method_whitelist；urllib3>=2 已改名 allowed_methods。做兼容补丁。
if "method_whitelist" not in _Retry.__init__.__code__.co_varnames:
    _orig_retry_init = _Retry.__init__

    def _patched_retry_init(self, *args, **kwargs):
        if "method_whitelist" in kwargs:
            kwargs["allowed_methods"] = kwargs.pop("method_whitelist")
        _orig_retry_init(self, *args, **kwargs)

    _Retry.__init__ = _patched_retry_init

from pytrends.request import TrendReq

from graph.paths import runtime_root
from .base import DataSource

CACHE_DIR = runtime_root() / ".cache" / "google_trends"
CACHE_TTL = 24 * 3600
CACHE_VERSION = "v3"


def _cache_fname(key: str, date_suffix: str = "") -> str:
    digest = hashlib.md5((CACHE_VERSION + "|" + key).encode("utf-8")).hexdigest()
    return f"{digest}.{date_suffix}.json" if date_suffix else f"{digest}.json"


def _cache_date(p: Path) -> datetime.date:
    """解析文件名里的 YYYYMMDD；无日期后缀则用 mtime。"""
    for token in p.name.split("."):
        if len(token) == 8 and token.isdigit():
            try:
                return datetime.datetime.strptime(token, "%Y%m%d").date()
            except ValueError:
                pass
    try:
        return datetime.date.fromtimestamp(p.stat().st_mtime)
    except Exception:
        return datetime.date.min


def _cache_paths(key: str) -> List[Path]:
    """该 key 的所有缓存文件（含旧版无日期后缀），按日期新旧降序。"""
    digest = hashlib.md5((CACHE_VERSION + "|" + key).encode("utf-8")).hexdigest()
    files = list(CACHE_DIR.glob(f"{digest}.*.json"))
    legacy = CACHE_DIR / f"{digest}.json"
    if legacy.exists():
        files.append(legacy)
    files.sort(key=_cache_date, reverse=True)
    return files


def _cache_get(key: str):
    """返回 24h 内有效的最新缓存；无则 None。"""
    for p in _cache_paths(key):
        try:
            fresh = (time.time() - p.stat().st_mtime) < CACHE_TTL
        except Exception:
            fresh = False
        if fresh:
            try:
                return json.loads(p.read_text(encoding="utf-8"))
            except Exception:
                continue
    return None


def _cache_latest(key: str):
    """取最新缓存文件内容（不限时效），用于抓取失败时的兜底（不删除缓存，取最新）。"""
    for p in _cache_paths(key):
        try:
            return json.loads(p.read_text(encoding="utf-8"))
        except Exception:
            continue
    return None


def _cache_set(key: str, data) -> None:
    """写当日新文件（保留历史，不覆盖）。"""
    CACHE_DIR.mkdir(parents=True, exist_ok=True)
    today = datetime.datetime.now().strftime("%Y%m%d")
    path = CACHE_DIR / _cache_fname(key, today)
    path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")


def _retry(func, max_attempts=3, base_delay=3):
    last = None
    for attempt in range(max_attempts):
        try:
            return func()
        except Exception as e:  # noqa: BLE001
            last = e
            if attempt == max_attempts - 1:
                break
            time.sleep(base_delay * (2 ** attempt))
    raise last if last else RuntimeError("retry failed")


def fetch_related(keywords, geo="US", timeframe="today 3-m"):
    """逐关键词串行请求 related_queries（单关键词接口，避免 429）。"""
    merged = {}
    for kw in keywords:
        time.sleep(3)  # 节流

        def _call(kw=kw):
            # timeout=(connect, read)：pytrends 默认 connect=2s 太短，网络波动即全挂，放宽到 10/30s
            pytrends = TrendReq(hl="en-US", tz=360, retries=2, backoff_factor=0.5, timeout=(10, 30))
            pytrends.build_payload(kw_list=[kw], timeframe=timeframe, geo=geo)
            return pytrends.related_queries()

        try:
            data = _retry(_call, max_attempts=2, base_delay=1)
        except Exception as e:  # noqa: BLE001
            print(f"[GoogleTrends] {geo} 种子「{kw}」抓取失败(跳过): {e}")
            continue
        if isinstance(data, dict):
            merged.update(data)
    return merged


def parse_related(raw, geo, source="gt_related"):
    rows = []
    for kw, payload in raw.items():
        if not isinstance(payload, dict):
            continue
        for kind in ("rising", "top"):
            df = payload.get(kind)
            if df is None or getattr(df, "empty", True):
                continue
            for _, r in df.iterrows():
                val = r["value"]
                if isinstance(val, str) and val.strip().lower() == "breakout":
                    num = 100.0
                else:
                    try:
                        num = float(val)
                    except (TypeError, ValueError):
                        continue
                rows.append({
                    "country": geo,
                    "topic": str(r["query"]).strip(),
                    "seed": kw,
                    "source": source,
                    "kind": kind,
                    "raw_score": num,
                })
    return rows


def _parse_traffic(desc):
    m = re.search(r"([\d,]+)\+?\s*searches", desc or "", re.I)
    if m:
        try:
            return float(m.group(1).replace(",", ""))
        except ValueError:
            return None
    return None


def fetch_trending(geo="US", limit=40):
    key = f"trending|{geo}|{limit}"
    cached = _cache_get(key)
    if cached is not None:
        return cached
    url = f"https://trends.google.com/trending/rss?geo={geo}"
    try:
        resp = requests.get(url, timeout=15, headers={"User-Agent": "Mozilla/5.0"})
        resp.raise_for_status()
        root = ET.fromstring(resp.content)
        rows = []
        for idx, it in enumerate(root.findall(".//item")[:limit]):
            title = (it.findtext("title") or "").strip()
            if not title:
                continue
            score = _parse_traffic(it.findtext("description"))
            if score is None:
                score = float(limit - idx)
            rows.append({
                "country": geo, "topic": title, "seed": "",
                "source": "gt_trending", "kind": "trending", "raw_score": score,
            })
        _cache_set(key, rows)
        return rows
    except Exception as e:  # noqa: BLE001
        print(f"[GoogleTrends 趋势] {geo} 抓取失败: {e}")
        latest = _cache_latest(key)
        if latest is not None:
            print(f"[GoogleTrends 趋势] {geo} 回退最新缓存({len(latest)}条)")
            return latest
        return []


def get_rows(keywords, geo="US", timeframe="today 3-m", source="gt_related"):
    key = f"{','.join(keywords)}|{geo}|{timeframe}|{source}|rows"
    cached = _cache_get(key)
    if cached is not None:
        return cached
    raw = fetch_related(keywords, geo=geo, timeframe=timeframe)
    rows = parse_related(raw, geo, source=source)
    if raw:  # 有结果才写当日新缓存
        _cache_set(key, rows)
        return rows
    # 抓取无果（429/超时）：回退最新历史缓存，保证流水线不中断
    latest = _cache_latest(key)
    if latest is not None:
        print(f"[GoogleTrends] {geo} 种子「{','.join(keywords)}」抓取无结果，回退最新缓存({len(latest)}行)")
        return latest
    return rows


class GoogleTrendsSource(DataSource):
    name = "google_trends"

    def fetch(self, country, country_config, global_config):
        cc = country_config or {}
        trending_cfg = cc.get("trending", {})
        style_cfg = cc.get("style", {})
        related_cfg = cc.get("related", {})
        tf = cc.get("timeframe", "today 3-m")

        rows: List[Dict[str, Any]] = []

        # 1) 国家实时趋势榜（主源）
        if trending_cfg.get("enabled", True):
            limit = int(trending_cfg.get("limit", 40))
            rows.extend(fetch_trending(geo=country, limit=limit))

        # 2) 风格种子词
        if style_cfg.get("enabled", True):
            seeds = style_cfg.get("seeds", []) or []
            if seeds:
                rows.extend(get_rows(seeds, geo=country, timeframe=tf, source="gt_style"))

        # 3) 行业种子词
        if related_cfg.get("enabled", True):
            seeds = related_cfg.get("seed_keywords", []) or []
            if seeds:
                rows.extend(get_rows(seeds, geo=country, timeframe=tf, source="gt_related"))

        return rows
