"""Proveedor de datos REAL vía el endpoint público de *syndication* de X.

Usa https://syndication.twitter.com/srv/timeline-profile/screen-name/<handle>,
el mismo que usan las webs para incrustar timelines. Ventajas:
  - Sin login y sin API key  -> cero riesgo para tu cuenta, gratis.
  - Sin el token anti-bot 'x-client-transaction-id' que rompe a otros scrapers.
  - Trae engagement real (favorite/retweet/reply/quote_count) y fecha.

Limitaciones conocidas (gestionadas aquí):
  - X devuelve un CACHÉ ANTIGUO para algunas cuentas. Lo DETECTAMOS (campo `stale`
    en `last_meta`) e intentamos un 'cache-bust' para forzar datos en vivo.
  - RATE-LIMIT (HTTP 429) si se piden muchas cuentas muy rápido -> pausa
    configurable entre peticiones y reintentos con backoff.

Tras cada `fetch_user_tweets`, `self.last_meta[handle]` guarda metadatos útiles
para la UI: {'newest': datetime|None, 'stale': bool, 'n_raw': int}.

Solo usa la librería estándar (urllib, json, re, time); no añade dependencias.
"""

import json
import re
import time
import urllib.error
import urllib.request
from datetime import datetime, timezone
from typing import List, Optional

from agent.models import Tweet
from agent.providers.base import TweetProvider

_UA = (
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
    "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
)
_NEXT_DATA = re.compile(
    r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', re.S
)
_BASE_URL = "https://syndication.twitter.com/srv/timeline-profile/screen-name/"


class SyndicationError(Exception):
    """Error al obtener/parsear datos del endpoint de syndication."""


def _parse_created_at(value: str) -> datetime:
    # Formato Twitter: "Tue Jan 24 20:14:18 +0000 2023" (tz-aware)
    return datetime.strptime(value, "%a %b %d %H:%M:%S %z %Y")


class SyndicationProvider(TweetProvider):
    def __init__(self, cfg: dict = None):
        cfg = cfg or {}
        self.delay = float(cfg.get("syndication_delay_seconds", 6))
        self.timeout = float(cfg.get("syndication_timeout", 25))
        self.retries = int(cfg.get("syndication_retries", 2))
        self.stale_after_days = float(cfg.get("syndication_stale_after_days", 7))
        self.cache_bust = bool(cfg.get("syndication_cache_bust", True))
        self._last_request = 0.0
        self.last_meta = {}  # handle -> {'newest', 'stale', 'n_raw'}

    # --- red ---
    def _throttle(self) -> None:
        wait = self.delay - (time.monotonic() - self._last_request)
        if wait > 0:
            time.sleep(wait)
        self._last_request = time.monotonic()

    def _build_url(self, handle: str) -> str:
        url = _BASE_URL + handle.lstrip("@")
        if self.cache_bust:
            # Un parámetro único intenta saltarse la caché de X y forzar datos en vivo.
            url += f"?dnt=1&_={int(time.time() * 1000)}"
        return url

    def _http_get(self, url: str) -> str:
        req = urllib.request.Request(url, headers={"User-Agent": _UA, "Accept": "text/html"})
        last_err = None
        for attempt in range(self.retries + 1):
            self._throttle()
            try:
                with urllib.request.urlopen(req, timeout=self.timeout) as resp:
                    return resp.read().decode("utf-8", "ignore")
            except urllib.error.HTTPError as e:
                last_err = e
                if e.code == 429 and attempt < self.retries:
                    time.sleep(self.delay * (attempt + 2))  # backoff progresivo
                    continue
                raise SyndicationError(
                    f"HTTP {e.code}" + (" (rate-limit)" if e.code == 429 else "")
                ) from e
            except Exception as e:  # red, timeout, etc.
                last_err = e
                if attempt < self.retries:
                    time.sleep(self.delay)
                    continue
        raise SyndicationError(f"sin respuesta tras reintentos: {last_err}")

    # --- parseo (separado para poder testearlo sin red) ---
    def _parse_all(self, html: str, handle: str) -> List[Tweet]:
        """Todos los tweets de la respuesta (orden descendente), SIN filtrar por fecha."""
        m = _NEXT_DATA.search(html)
        if not m:
            raise SyndicationError("no se encontró __NEXT_DATA__ (¿X cambió el formato?)")
        data = json.loads(m.group(1))
        try:
            entries = data["props"]["pageProps"]["timeline"]["entries"]
        except (KeyError, TypeError):
            raise SyndicationError("estructura inesperada en la respuesta")

        out: List[Tweet] = []
        for entry in entries:
            tw = (entry.get("content") or {}).get("tweet")
            if not tw or "id_str" not in tw:
                continue
            try:
                created = _parse_created_at(tw["created_at"])
            except (KeyError, ValueError):
                continue
            author = (tw.get("user") or {}).get("screen_name") or handle.lstrip("@")
            views = tw.get("view_count")
            out.append(
                Tweet(
                    id=str(tw["id_str"]),
                    author=author,
                    text=tw.get("full_text") or tw.get("text") or "",
                    created_at=created,
                    likes=int(tw.get("favorite_count") or 0),
                    retweets=int(tw.get("retweet_count") or 0),
                    replies=int(tw.get("reply_count") or 0),
                    quotes=int(tw.get("quote_count") or 0),
                    views=int(views) if str(views).isdigit() else 0,
                    url=f"https://x.com/{author}/status/{tw['id_str']}",
                )
            )
        out.sort(key=lambda t: t.created_at, reverse=True)
        return out

    def parse_html(self, html: str, handle: str, since: datetime, limit: int = 50) -> List[Tweet]:
        """Tweets dentro de la ventana [since, now]."""
        return [t for t in self._parse_all(html, handle) if t.created_at >= since][:limit]

    def is_stale(self, newest: Optional[datetime]) -> bool:
        if newest is None:
            return True
        return (datetime.now(timezone.utc) - newest).days >= self.stale_after_days

    def fetch_user_tweets(self, handle: str, since: datetime, limit: int = 50) -> List[Tweet]:
        html = self._http_get(self._build_url(handle))
        all_tweets = self._parse_all(html, handle)
        newest = all_tweets[0].created_at if all_tweets else None
        self.last_meta[handle.lstrip("@")] = {
            "newest": newest,
            "stale": self.is_stale(newest),
            "n_raw": len(all_tweets),
        }
        return [t for t in all_tweets if t.created_at >= since][:limit]
