"""Cacheo en disco para cualquier proveedor.

Envuelve a otro TweetProvider y guarda en disco (por handle) los tweets traídos,
con un TTL. Así la app no vuelve a pedir a X en cada recarga, lo que es CLAVE con
Scweet para no agotar la cuota de la cuenta secundaria (rate-limit).

Cachea un conjunto amplio por handle (sin filtrar por fecha) y filtra por la
ventana en lectura, de modo que una misma caché sirve para cualquier 'window'.
"""

import json
import os
import time
from datetime import datetime, timezone
from typing import List

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

_EPOCH = datetime.fromtimestamp(0, tz=timezone.utc)


def _tweet_to_dict(t: Tweet) -> dict:
    return {
        "id": t.id, "author": t.author, "text": t.text,
        "created_at": t.created_at.isoformat(),
        "likes": t.likes, "retweets": t.retweets, "replies": t.replies,
        "quotes": t.quotes, "views": t.views, "url": t.url,
    }


def _tweet_from_dict(d: dict) -> Tweet:
    return Tweet(
        id=d["id"], author=d["author"], text=d["text"],
        created_at=datetime.fromisoformat(d["created_at"]),
        likes=d.get("likes", 0), retweets=d.get("retweets", 0),
        replies=d.get("replies", 0), quotes=d.get("quotes", 0),
        views=d.get("views", 0), url=d.get("url", ""),
    )


class CachedProvider(TweetProvider):
    def __init__(self, inner: TweetProvider, cache_dir: str, ttl_minutes: float = 60):
        self.inner = inner
        self.cache_dir = cache_dir
        self.ttl = float(ttl_minutes) * 60
        os.makedirs(cache_dir, exist_ok=True)
        self.last_meta = {}

    def _path(self, handle: str) -> str:
        safe = handle.lstrip("@").lower().replace("/", "_").replace("\\", "_")
        return os.path.join(self.cache_dir, f"{safe}.json")

    def _set_meta(self, handle: str, meta: dict):
        newest = meta.get("newest")
        if isinstance(newest, str):
            try:
                meta = {**meta, "newest": datetime.fromisoformat(newest)}
            except ValueError:
                meta = {**meta, "newest": None}
        self.last_meta[handle.lstrip("@")] = meta

    @staticmethod
    def _window(tweets: List[Tweet], since: datetime, limit: int) -> List[Tweet]:
        out = [t for t in tweets if t.created_at >= since]
        out.sort(key=lambda t: t.created_at, reverse=True)
        return out[:limit]

    def fetch_user_tweets(self, handle: str, since: datetime, limit: int = 50) -> List[Tweet]:
        path = self._path(handle)

        # 1) caché fresca en disco
        if os.path.exists(path):
            try:
                with open(path, encoding="utf-8") as f:
                    blob = json.load(f)
                if time.time() - blob.get("fetched_at", 0) < self.ttl:
                    self._set_meta(handle, dict(blob.get("meta", {})))
                    tweets = [_tweet_from_dict(d) for d in blob.get("tweets", [])]
                    return self._window(tweets, since, limit)
            except Exception:
                pass

        # 2) en vivo (amplio: since=epoch para cachear todo lo reciente)
        tweets = self.inner.fetch_user_tweets(handle, since=_EPOCH, limit=limit)
        meta = dict(getattr(self.inner, "last_meta", {}).get(handle.lstrip("@"), {}))
        self.last_meta[handle.lstrip("@")] = meta

        # Solo cacheamos si hubo datos: así un fetch vacío por rate-limit no
        # congela "0 tweets" durante todo el TTL.
        if tweets:
            try:
                meta_save = dict(meta)
                if isinstance(meta_save.get("newest"), datetime):
                    meta_save["newest"] = meta_save["newest"].isoformat()
                with open(path, "w", encoding="utf-8") as f:
                    json.dump(
                        {"fetched_at": time.time(),
                         "tweets": [_tweet_to_dict(t) for t in tweets],
                         "meta": meta_save},
                        f, ensure_ascii=False,
                    )
            except Exception:
                pass

        return self._window(tweets, since, limit)

    def clear(self) -> None:
        if not os.path.isdir(self.cache_dir):
            return
        for fn in os.listdir(self.cache_dir):
            if fn.endswith(".json"):
                try:
                    os.remove(os.path.join(self.cache_dir, fn))
                except OSError:
                    pass
