"""Captación de interés de búsqueda de Google Trends vía pytrends.

Google Trends NUNCA devuelve volúmenes absolutos de búsqueda: todo el interés es
relativo y está normalizado en una escala 0-100 dentro del payload consultado
(keywords + geo + timeframe). Esta función aplana el resultado de pytrends en una
lista de dicts con un schema fijo que casa 1:1 con la tabla Postgres
`google_trends`.
"""

import time


# Sentinel numérico para related_queries "rising" que Google marca como "Breakout".
# pytrends entrega la cadena literal "Breakout" cuando el crecimiento es tan alto
# que no cabe en un porcentaje (>5000%). Lo representamos como este entero para
# mantener la columna `value` numérica en Postgres sin perder la señal.
BREAKOUT_SENTINEL = 999999


def _to_iso(value) -> str:
    """Convierte una fecha/timestamp de pandas a ISO YYYY-MM-DD."""
    # pandas Timestamp y datetime.date/datetime exponen strftime.
    if hasattr(value, "strftime"):
        return value.strftime("%Y-%m-%d")
    # Fallback: ya viene como string ISO o similar; recorta a 10 chars (fecha).
    return str(value)[:10]


def _coerce_value(raw):
    """Normaliza el valor de una related_query rising/top a int o sentinel.

    pytrends devuelve enteros para top y la mayoría de rising, pero rising puede
    traer la cadena "Breakout". Cualquier valor no numérico se mapea al sentinel.
    """
    if isinstance(raw, str):
        if raw.strip().lower() == "breakout":
            return BREAKOUT_SENTINEL
        try:
            return int(float(raw))
        except (ValueError, TypeError):
            return BREAKOUT_SENTINEL
    try:
        return int(raw)
    except (ValueError, TypeError):
        return None


def scrape_google_trends(
    keywords: list[str],
    geo: str = "ES",
    timeframe: str = "now 7-d",
    include_related: bool = True,
) -> list[dict]:
    """Capta interés de búsqueda de Google Trends para una lista de keywords.

    Construye un único payload de pytrends (keywords + geo + timeframe) y aplana
    interest_over_time y, opcionalmente, related_queries (rising + top) en filas
    homogéneas. El interés es relativo 0-100, nunca volumen absoluto.

    Args:
        keywords: lista de términos/nichos a consultar (máx. 5 por payload — límite
            de Google Trends). Cada elemento es una keyword.
        geo: código de país ISO-3166 (ej. "ES", "US", "" para mundial).
        timeframe: ventana temporal en sintaxis pytrends (ej. "now 7-d",
            "today 3-m", "today 12-m", "2024-01-01 2024-12-31").
        include_related: si True, añade filas metric="rising" y metric="top" de
            related_queries por keyword. Si False, solo interest_over_time.

    Returns:
        Lista de dicts con EXACTAMENTE estas claves (sin id/snapshot_date/scraped_at,
        que los añade el ingest):
            geo, timeframe, keyword, metric, point_date, value, related_query
        Tres familias de fila según `metric`:
          - "interest_over_time": una por (keyword, punto temporal). point_date=fecha
            ISO, value=interés 0-100, related_query=None.
          - "rising": related_queries rising (si include_related). related_query=query,
            value=valor rising (Breakout→BREAKOUT_SENTINEL), point_date=None.
          - "top": related_queries top (si include_related). related_query=query,
            value=valor 0-100, point_date=None.

    Raises:
        RuntimeError: si Google rate-limitea (429) tras agotar los reintentos, o si
            pytrends falla de forma no recuperable.
    """
    # Import dentro de la función: pytrends es dependencia impura/externa.
    from pytrends.request import TrendReq

    if not keywords:
        return []

    pytrends = TrendReq(hl="es-ES", tz=60)

    # ---- build_payload con backoff ante 429 ----
    backoff = [5, 15, 30]
    last_err = None
    for attempt in range(len(backoff) + 1):
        try:
            pytrends.build_payload(keywords, geo=geo, timeframe=timeframe)
            last_err = None
            break
        except Exception as exc:  # pragma: no cover - depende de la red
            last_err = exc
            msg = str(exc).lower()
            is_rate_limit = "429" in msg or "too many requests" in msg or "rate" in msg
            if attempt < len(backoff) and is_rate_limit:
                time.sleep(backoff[attempt])
                continue
            if is_rate_limit:
                raise RuntimeError(
                    "Google Trends rate-limited (429): se agotaron los reintentos "
                    f"({len(backoff)} backoffs {backoff}s). pytrends usa una API no "
                    "oficial y Google la limita agresivamente. Reintenta más tarde."
                ) from exc
            raise RuntimeError(
                f"build_payload falló de forma no recuperable: {exc}"
            ) from exc
    if last_err is not None:
        raise RuntimeError(f"build_payload no completó: {last_err}")

    rows: list[dict] = []

    # ---- interest_over_time ----
    try:
        iot = pytrends.interest_over_time()
    except Exception as exc:  # pragma: no cover - depende de la red
        raise RuntimeError(f"interest_over_time falló: {exc}") from exc

    if iot is not None and not iot.empty:
        # El índice es la fecha; cada columna es una keyword + 'isPartial' (ignorar).
        for idx, record in iot.iterrows():
            point_date = _to_iso(idx)
            for kw in keywords:
                if kw not in record:
                    continue
                rows.append(
                    {
                        "geo": geo,
                        "timeframe": timeframe,
                        "keyword": kw,
                        "metric": "interest_over_time",
                        "point_date": point_date,
                        "value": int(record[kw]),
                        "related_query": None,
                    }
                )

    # ---- related_queries (rising + top) ----
    if include_related:
        try:
            related = pytrends.related_queries()
        except Exception as exc:  # pragma: no cover - depende de la red
            raise RuntimeError(f"related_queries falló: {exc}") from exc

        related = related or {}
        for kw in keywords:
            entry = related.get(kw) or {}
            for metric in ("rising", "top"):
                df = entry.get(metric)
                if df is None or getattr(df, "empty", True):
                    continue
                for _, qrow in df.iterrows():
                    rows.append(
                        {
                            "geo": geo,
                            "timeframe": timeframe,
                            "keyword": kw,
                            "metric": metric,
                            "point_date": None,
                            "value": _coerce_value(qrow.get("value")),
                            "related_query": qrow.get("query"),
                        }
                    )

    return rows


if __name__ == "__main__":
    # Self-test: el import siempre debe funcionar. Una llamada real a Google puede
    # dar 429 en este entorno; la capturamos y reportamos sin fallar.
    print("import OK")
    try:
        out = scrape_google_trends(["python", "rust"], geo="ES", timeframe="now 7-d")
        n_iot = sum(1 for r in out if r["metric"] == "interest_over_time")
        n_rising = sum(1 for r in out if r["metric"] == "rising")
        n_top = sum(1 for r in out if r["metric"] == "top")
        print(
            f"ok: {len(out)} filas "
            f"(interest_over_time={n_iot}, rising={n_rising}, top={n_top})"
        )
        if out:
            print("muestra:", out[0])
    except RuntimeError as exc:
        print(f"rate-limited o error de red (esperado en este entorno): {exc}")
