"""
wivexx_socials.py — social media growth trackers  (v1.12)
═══════════════════════════════════════════════════════════════════════
Track social accounts (Instagram first) the way an analyst would:

  • SNAPSHOTS — every check stores followers / following / post count plus
    the ~12 most recent posts (likes, comments, video views).  History lives
    in wivexx_socials.json, so growth is computed from REAL data points,
    not estimates.
  • WEEKLY GROWTH — follower delta vs the snapshot closest to 7 days ago
    (or the oldest one available while history is still building).
  • NEW COMMENTS — comment-count delta across the recent posts since the
    baseline snapshot, including comments on brand-new posts.
  • LATEST VIDEO/POST PERFORMANCE — views/likes/comments of the newest
    post, compared against the median of the previous recent posts so
    "good" actually means something ("40 percent above your recent average").
  • ENGAGEMENT RATE — (likes+comments)/followers on recent posts, the
    number brands/sponsors actually look at.
  • POSTING CADENCE — days since the last post (consistency is the single
    biggest organic-growth lever).

Data source: Instagram's public web profile endpoint (no login, public
profiles only) with the documented X-IG-App-ID header; falls back to the
`instaloader` package if installed.  Instagram rate-limits anonymous
reads, so WIVEXX checks at most once per account per CHECK_COOLDOWN.

Public API:
    list_trackers() -> list[dict]
    add_tracker(platform, handle) -> dict          (raises ValueError)
    remove_tracker(platform, handle) -> bool
    take_snapshot(platform, handle, force=False) -> Optional[dict]
    report(platform, handle) -> Optional[dict]     (numbers, for GUI/web)
    spoken_report(handle=None) -> str              (for the voice query)
    start_session_briefing(announce, logger)       (once per WIVEXX session)
"""

import datetime as _dt
import json
import os
import re
import statistics
import threading
import time
import urllib.parse
import urllib.request
from typing import Callable, Optional

_SOCIALS_FILE = os.path.join(
    os.path.dirname(os.path.abspath(__file__)), "wivexx_socials.json"
)
_lock = threading.Lock()
_logger = None

PLATFORMS = ("instagram",)

CHECK_COOLDOWN = 1800        # seconds between live fetches per account
MAX_SNAPSHOTS = 240          # per account (~weeks of history)
_BASELINE_DAYS = 7           # "last week" growth window

_briefed_this_session = False

# Why the last fetch for a handle failed: "not_found" | "rate_limited" |
# "private" | "offline" — lets WIVEXX say something USEFUL instead of a
# generic "couldn't read it".
_LAST_ERROR: dict = {}


def fetch_error(handle: str) -> str:
    return _LAST_ERROR.get(_clean_handle(handle), "")


def explain_fetch_error(handle: str) -> str:
    """Human sentence for the last failure reason."""
    reason = fetch_error(handle)
    h = _clean_handle(handle)
    if reason == "not_found":
        return (f"Instagram reports that @{h} does not exist — double-check "
                f"the spelling. If you're sure it's right, Instagram may be "
                f"temporarily blocking reads from this PC and I'll keep "
                f"trying.")
    if reason == "rate_limited":
        return ("Instagram is rate-limiting reads right now — "
                "I'll retry automatically later.")
    if reason == "login":
        return ("My Instagram session isn't valid — run "
                "'instaloader --login' again from a terminal.")
    if reason == "private":
        return (f"@{h} is private, so I can only see the follower count.")
    return ("Instagram isn't reachable right now — it may be rate-limiting, "
            "or the connection is down.")


# ── Storage ────────────────────────────────────────────────────────────────────
def _load() -> dict:
    try:
        with open(_SOCIALS_FILE, encoding="utf-8") as f:
            data = json.load(f)
        if isinstance(data, dict) and isinstance(data.get("accounts"), list):
            data.setdefault("snapshots", {})
            return data
    except Exception:
        pass
    return {"accounts": [], "snapshots": {}}


def _save(data: dict) -> None:
    try:
        with open(_SOCIALS_FILE, "w", encoding="utf-8") as f:
            json.dump(data, f, indent=2)
    except Exception as e:
        if _logger:
            _logger.warning("socials: save failed: %s", e)


def _key(platform: str, handle: str) -> str:
    return f"{platform.lower()}:{handle.lower()}"


def _clean_handle(handle: str) -> str:
    return (handle or "").strip().lstrip("@").strip(" .,!?").lower()


# ── Tracker management ─────────────────────────────────────────────────────────
def list_trackers() -> list:
    with _lock:
        return [dict(a) for a in _load()["accounts"]]


def add_tracker(platform: str, handle: str) -> dict:
    platform = (platform or "instagram").lower()
    handle = _clean_handle(handle)
    if platform not in PLATFORMS:
        raise ValueError(f"unsupported platform: {platform}")
    if not re.fullmatch(r"[a-z0-9._]{1,30}", handle):
        raise ValueError(f"that doesn't look like a valid {platform} handle")
    with _lock:
        data = _load()
        for a in data["accounts"]:
            if _key(a["platform"], a["handle"]) == _key(platform, handle):
                return dict(a)
        acct = {"platform": platform, "handle": handle,
                "added": _dt.datetime.now().strftime("%Y-%m-%d %H:%M")}
        data["accounts"].append(acct)
        _save(data)
    if _logger:
        _logger.info("socials: now tracking %s @%s", platform, handle)
    return dict(acct)


def remove_tracker(platform: str, handle: str) -> bool:
    handle = _clean_handle(handle)
    with _lock:
        data = _load()
        before = len(data["accounts"])
        data["accounts"] = [
            a for a in data["accounts"]
            if not (a["platform"] == platform.lower()
                    and a["handle"] == handle)
        ]
        if len(data["accounts"]) != before:
            data["snapshots"].pop(_key(platform, handle), None)
            _save(data)
            if _logger:
                _logger.info("socials: stopped tracking %s @%s", platform, handle)
            return True
    return False


def find_tracker(handle: str) -> Optional[dict]:
    """Loose match: '@handle' / 'handle' on any platform."""
    handle = _clean_handle(handle)
    for a in list_trackers():
        if a["handle"] == handle:
            return a
    return None


# ── Instagram fetch (public profiles, no login) ───────────────────────────────
_UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
       "AppleWebKit/537.36 (KHTML, like Gecko) "
       "Chrome/124.0.0.0 Safari/537.36")
_IG_HEADERS = {
    "User-Agent": _UA,
    "X-IG-App-ID": "936619743392459",
    "Accept": "*/*",
    "Accept-Language": "en-US,en;q=0.9",
    "Accept-Encoding": "gzip, deflate",
    "Referer": "https://www.instagram.com/",
    "Sec-Fetch-Site": "same-origin",
    "Sec-Fetch-Mode": "cors",
    "Sec-Fetch-Dest": "empty",
}


def _http_get(url: str, headers: dict, timeout: float = 12) -> bytes:
    req = urllib.request.Request(url, headers=headers)
    with urllib.request.urlopen(req, timeout=timeout) as resp:
        raw = resp.read()
        enc = (resp.headers.get("Content-Encoding") or "").lower()
    if enc == "gzip":
        import gzip
        raw = gzip.decompress(raw)
    elif enc == "deflate":
        import zlib
        raw = zlib.decompress(raw)
    return raw


def _fetch_instagram_endpoint(handle: str) -> Optional[dict]:
    """Instagram's public web_profile_info endpoint → normalised profile dict.
    Works for PUBLIC profiles without login; anonymously rate-limited
    (HTTP 429) — callers fall back to the HTML scrape below."""
    for host in ("www.instagram.com", "i.instagram.com"):
        url = (f"https://{host}/api/v1/users/web_profile_info/"
               f"?username={urllib.parse.quote(handle)}")
        try:
            hdrs = dict(_IG_HEADERS)
            hdrs["Referer"] = f"https://www.instagram.com/{handle}/"
            payload = json.loads(
                _http_get(url, hdrs).decode("utf-8", "replace"))
            user = (payload.get("data") or {}).get("user") or {}
            if not user:
                continue
            recent = []
            edges = ((user.get("edge_owner_to_timeline_media") or {})
                     .get("edges") or [])
            for e in edges[:12]:
                n = e.get("node") or {}
                cap_edges = ((n.get("edge_media_to_caption") or {})
                             .get("edges") or [])
                caption = ""
                if cap_edges:
                    caption = (cap_edges[0].get("node") or {}).get("text", "")
                likes = ((n.get("edge_liked_by") or {}).get("count")
                         or (n.get("edge_media_preview_like") or {}).get("count")
                         or 0)
                recent.append({
                    "id": str(n.get("id", "")),
                    "code": n.get("shortcode", ""),
                    "video": bool(n.get("is_video")),
                    "likes": int(likes or 0),
                    "comments": int((n.get("edge_media_to_comment") or {})
                                    .get("count") or 0),
                    "views": int(n.get("video_view_count") or 0),
                    "taken_at": int(n.get("taken_at_timestamp") or 0),
                    "caption": caption[:120],
                })
            _LAST_ERROR.pop(_clean_handle(handle), None)
            return {
                "followers": int((user.get("edge_followed_by") or {})
                                 .get("count") or 0),
                "following": int((user.get("edge_follow") or {})
                                 .get("count") or 0),
                "posts": int((user.get("edge_owner_to_timeline_media") or {})
                             .get("count") or 0),
                "private": bool(user.get("is_private")),
                "bio": str(user.get("biography") or "")[:400],
                "full_name": str(user.get("full_name") or "")[:80],
                "recent": recent,
            }
        except Exception as e:
            msg = str(e)
            h = _clean_handle(handle)
            if "404" in msg:
                _LAST_ERROR[h] = "not_found"
            elif "429" in msg or "401" in msg:
                _LAST_ERROR.setdefault(h, "rate_limited")
            else:
                _LAST_ERROR.setdefault(h, "offline")
            if _logger:
                _logger.debug("socials: IG endpoint via %s failed: %s", host, e)
    return None


_OG_COUNT_RE = re.compile(
    r"([\d.,]+)\s*([KkMmBb]?)\s*Followers?,\s*([\d.,]+)\s*([KkMmBb]?)\s*Following,"
    r"\s*([\d.,]+)\s*([KkMmBb]?)\s*Posts?", re.IGNORECASE)


def _og_num(num: str, suffix: str) -> int:
    n = float(num.replace(",", ""))
    mult = {"k": 1_000, "m": 1_000_000, "b": 1_000_000_000}.get(
        suffix.lower(), 1)
    return int(n * mult)


def _fetch_instagram_html(handle: str) -> Optional[dict]:
    """Last-ditch no-login fallback: the profile page's og:description meta
    tag ('1.2M Followers, 300 Following, 4,200 Posts — …').  Survives the
    JSON endpoint's anonymous rate limit, but carries no per-post stats —
    follower growth still works, comment/video stats resume when the
    endpoint lets us back in."""
    for url in (f"https://www.instagram.com/{urllib.parse.quote(handle)}/embed/",
                f"https://www.instagram.com/{urllib.parse.quote(handle)}/"):
        try:
            html = _http_get(url, {
                "User-Agent": _UA,
                "Accept": "text/html,application/xhtml+xml",
                "Accept-Language": "en-US,en;q=0.9",
                "Accept-Encoding": "gzip, deflate",
            }).decode("utf-8", "replace")
            m = _OG_COUNT_RE.search(html)
            if m:
                return {
                    "followers": _og_num(m.group(1), m.group(2)),
                    "following": _og_num(m.group(3), m.group(4)),
                    "posts": _og_num(m.group(5), m.group(6)),
                    "private": False,
                    "recent": [],
                }
        except Exception as e:
            if _logger:
                _logger.debug("socials: IG html fallback (%s) failed: %s",
                              url, e)
    return None


def _config_login() -> str:
    try:
        import wivexx_config as _cfg
        return (getattr(_cfg, "INSTAGRAM_LOGIN", "") or "").strip()
    except Exception:
        return ""


def _fetch_instagram_instaloader(handle: str) -> Optional[dict]:
    """The RELIABLE path when the `instaloader` package is installed.
    If INSTAGRAM_LOGIN is set in wivexx_config.py and a saved instaloader
    session exists (run `instaloader --login <user>` once), reads are
    authenticated; anonymous reads are heavily rate-limited by Instagram.
    Fails FAST (one connection attempt) — a voice turn must never hang in
    instaloader's multi-minute retry loop."""
    h = _clean_handle(handle)
    try:
        import instaloader  # type: ignore
    except ImportError:
        return None
    try:
        L = instaloader.Instaloader(quiet=True,
                                    download_pictures=False,
                                    download_videos=False,
                                    download_video_thumbnails=False,
                                    save_metadata=False,
                                    max_connection_attempts=1)
        login = _config_login()
        if login:
            try:
                L.load_session_from_file(login)
            except Exception as _se:
                _LAST_ERROR[h] = "login"
                if _logger:
                    _logger.warning("socials: could not load instaloader "
                                    "session for %s: %s — run "
                                    "'instaloader --login %s'",
                                    login, _se, login)
        profile = instaloader.Profile.from_username(L.context, handle)
        recent = []
        try:
            if not profile.is_private:
                for post in profile.get_posts():
                    recent.append({
                        "id": str(post.mediaid),
                        "code": post.shortcode,
                        "video": bool(post.is_video),
                        "likes": int(post.likes or 0),
                        "comments": int(post.comments or 0),
                        "views": int(getattr(post, "video_view_count", 0) or 0),
                        "taken_at": int(post.date_utc.timestamp()),
                        "caption": (post.caption or "")[:300],
                    })
                    if len(recent) >= 12:
                        break
        except Exception as _pe:
            if _logger:
                _logger.debug("socials: instaloader posts read stopped: %s", _pe)
        _LAST_ERROR.pop(h, None)
        return {
            "followers": int(profile.followers),
            "following": int(profile.followees),
            "posts": int(profile.mediacount),
            "private": bool(profile.is_private),
            "bio": (profile.biography or "")[:400],
            "full_name": (profile.full_name or "")[:80],
            "recent": recent,
        }
    except Exception as e:
        name = type(e).__name__
        msg = str(e)
        if "ProfileNotExists" in name or "does not exist" in msg:
            _LAST_ERROR[h] = "not_found"
        elif "LoginRequired" in name or "login" in msg.lower():
            _LAST_ERROR[h] = "login"
        elif "401" in msg or "429" in msg or "wait a few minutes" in msg.lower():
            _LAST_ERROR[h] = "rate_limited"
        else:
            _LAST_ERROR.setdefault(h, "offline")
        if _logger:
            _logger.warning("socials: instaloader %s @%s: %s", name, h, msg)
        return None


def fetch_profile(platform: str, handle: str) -> Optional[dict]:
    if platform == "instagram":
        if _config_login():
            # authenticated instaloader session = the reliable path; try it
            # FIRST so we don't waste (and heat up) anonymous requests.
            # NOTE: even "profile does not exist" is NOT trusted as final —
            # Instagram's temporary blocks can masquerade as not-found, so
            # the anonymous fallbacks still get a chance to disprove it.
            prof = _fetch_instagram_instaloader(handle)
            if prof is not None:
                return prof
            return (_fetch_instagram_endpoint(handle)
                    or _fetch_instagram_html(handle))
        return (_fetch_instagram_endpoint(handle)
                or _fetch_instagram_instaloader(handle)
                or _fetch_instagram_html(handle))
    return None


def profile_context(handle: str) -> Optional[dict]:
    """Compact profile dossier for the caption studio: bio, niche signals,
    voice sample from recent captions, engagement.  Uses the snapshot cache
    (cooldown-aware) so it never hammers Instagram."""
    handle = _clean_handle(handle)
    snap = take_snapshot("instagram", handle)
    if snap is None:
        return None
    recent = snap.get("recent") or []
    captions = [p.get("caption", "").strip() for p in recent
                if p.get("caption", "").strip()][:4]
    eng = None
    if snap.get("followers") and recent:
        inter = [(p.get("likes", 0) + p.get("comments", 0)) for p in recent]
        eng = round(100.0 * statistics.mean(inter) / snap["followers"], 2)
    return {
        "handle": handle,
        "full_name": snap.get("full_name", ""),
        "bio": snap.get("bio", ""),
        "followers": snap.get("followers", 0),
        "engagement_rate": eng,
        "caption_samples": captions,
        "private": snap.get("private", False),
    }


# ── Snapshots ──────────────────────────────────────────────────────────────────
def take_snapshot(platform: str, handle: str, force: bool = False) -> Optional[dict]:
    """Fetch live data and append a snapshot.  Returns the new snapshot, the
    most recent cached one when inside the cooldown, or None on failure."""
    handle = _clean_handle(handle)
    k = _key(platform, handle)
    with _lock:
        snaps = _load()["snapshots"].get(k, [])
    if snaps and not force:
        if time.time() - snaps[-1]["ts"] < CHECK_COOLDOWN:
            return dict(snaps[-1])

    prof = fetch_profile(platform, handle)
    if prof is None:
        if _logger:
            _logger.warning("socials: could not fetch %s @%s (%s)",
                            platform, handle,
                            fetch_error(handle) or "unknown")
        return dict(snaps[-1]) if snaps else None

    snap = {"ts": time.time(),
            "date": _dt.datetime.now().strftime("%Y-%m-%d %H:%M"),
            **prof}
    with _lock:
        data = _load()
        arr = data["snapshots"].setdefault(k, [])
        arr.append(snap)
        del arr[:-MAX_SNAPSHOTS]
        _save(data)
    if _logger:
        _logger.info("socials: snapshot %s @%s — %s followers",
                     platform, handle, prof["followers"])
    return dict(snap)


def _baseline(snaps: list, days: int = _BASELINE_DAYS) -> Optional[dict]:
    """The snapshot closest to *days* ago (prefer slightly older), or the
    oldest available while history is still shorter than the window."""
    if len(snaps) < 2:
        return None
    target = time.time() - days * 86400
    best = min(snaps[:-1], key=lambda s: abs(s["ts"] - target))
    return best


# ── Reports ────────────────────────────────────────────────────────────────────
def report(platform: str, handle: str) -> Optional[dict]:
    """Numbers for one account: current stats + growth vs baseline + latest
    post/video performance.  None when no data could ever be fetched."""
    handle = _clean_handle(handle)
    k = _key(platform, handle)
    latest = take_snapshot(platform, handle)
    if latest is None:
        return None
    with _lock:
        snaps = _load()["snapshots"].get(k, [])

    base = _baseline(snaps)
    out: dict = {
        "platform": platform, "handle": handle,
        "followers": latest["followers"],
        "following": latest.get("following", 0),
        "posts": latest.get("posts", 0),
        "private": latest.get("private", False),
        "checked": latest.get("date", ""),
        "follower_delta": None, "baseline_days": None,
        "new_comments": None, "new_posts": None,
        "latest": None, "latest_vs_typical": None,
        "engagement_rate": None, "days_since_post": None,
    }

    recent = latest.get("recent") or []
    if recent:
        newest = max(recent, key=lambda p: p.get("taken_at", 0))
        out["latest"] = dict(newest)
        if newest.get("taken_at"):
            out["days_since_post"] = max(
                0, int((time.time() - newest["taken_at"]) // 86400))
        if latest["followers"]:
            inter = [(p.get("likes", 0) + p.get("comments", 0))
                     for p in recent]
            out["engagement_rate"] = round(
                100.0 * statistics.mean(inter) / latest["followers"], 2)
        # newest post vs the median of the other recent posts
        others = [p for p in recent if p is not newest]
        metric = "views" if newest.get("video") and newest.get("views") else "likes"
        vals = [p.get(metric, 0) for p in others
                if (p.get("video") == newest.get("video") or metric == "likes")]
        vals = [v for v in vals if v > 0]
        if vals and newest.get(metric, 0) > 0:
            med = statistics.median(vals)
            if med > 0:
                out["latest_vs_typical"] = round(
                    100.0 * (newest[metric] - med) / med)
                out["latest_metric"] = metric

    if base is not None:
        out["follower_delta"] = latest["followers"] - base["followers"]
        out["baseline_days"] = max(1, round((latest["ts"] - base["ts"]) / 86400))
        # new comments: delta on posts present in both + all comments on new posts
        base_by_id = {p["id"]: p for p in (base.get("recent") or [])}
        new_comments = 0
        new_posts = 0
        for p in recent:
            old = base_by_id.get(p["id"])
            if old is None:
                new_posts += 1
                new_comments += p.get("comments", 0)
            else:
                new_comments += max(0, p.get("comments", 0)
                                    - old.get("comments", 0))
        out["new_comments"] = new_comments
        out["new_posts"] = new_posts
    return out


def history(platform: str, handle: str, days: int = 31) -> list:
    """Time-series of stored snapshots within the last *days* — for the GUI
    graphs.  Each point: {ts, date, followers, following, posts}.  No network
    call; reads only what's already cached."""
    handle = _clean_handle(handle)
    k = _key(platform, handle)
    with _lock:
        snaps = list(_load()["snapshots"].get(k, []))
    cutoff = time.time() - max(1, days) * 86400
    out = []
    for s in snaps:
        if s.get("ts", 0) < cutoff:
            continue
        out.append({
            "ts": s.get("ts", 0),
            "date": s.get("date", ""),
            "followers": s.get("followers", 0),
            "following": s.get("following", 0),
            "posts": s.get("posts", 0),
        })
    return out


def report_all(refresh: bool = False) -> list:
    """One report() per tracked account (numbers for the GUI cards).  When
    *refresh* is False, avoids forcing a live fetch unless the cooldown allows
    it (report() already respects the cooldown via take_snapshot)."""
    out = []
    for t in list_trackers():
        try:
            r = report(t["platform"], t["handle"])
        except Exception:
            r = None
        if r is None:
            out.append({"platform": t["platform"], "handle": t["handle"],
                        "error": explain_fetch_error(t["handle"]).rstrip(".")})
        else:
            out.append(r)
    return out


def _fmt_n(n) -> str:
    try:
        n = int(n)
    except Exception:
        return str(n)
    if abs(n) >= 1_000_000:
        return f"{n / 1_000_000:.1f} million"
    if abs(n) >= 10_000:
        return f"{n / 1000:.1f} thousand"
    return f"{n:,}"


def _spoken_one(r: dict) -> str:
    """One account → one WIVEXX-style spoken paragraph."""
    h = r["handle"]
    parts = [f"@{h} stands at {_fmt_n(r['followers'])} followers"]
    if r.get("follower_delta") is not None:
        d, days = r["follower_delta"], r.get("baseline_days") or _BASELINE_DAYS
        span = "this week" if days >= 6 else f"in the last {days} day{'s' if days != 1 else ''}"
        if d > 0:
            parts.append(f"up {_fmt_n(d)} {span}")
        elif d < 0:
            parts.append(f"down {_fmt_n(-d)} {span}")
        else:
            parts.append(f"flat {span}")
    if r.get("new_comments") is not None:
        parts.append(f"{_fmt_n(r['new_comments'])} new comments")
    if r.get("new_posts"):
        parts.append(f"{r['new_posts']} new post{'s' if r['new_posts'] != 1 else ''}")
    line = ", ".join(parts) + "."

    latest = r.get("latest")
    if latest:
        kind = "video" if latest.get("video") else "post"
        stats = []
        if latest.get("views"):
            stats.append(f"{_fmt_n(latest['views'])} views")
        if latest.get("likes"):
            stats.append(f"{_fmt_n(latest['likes'])} likes")
        if latest.get("comments"):
            stats.append(f"{_fmt_n(latest['comments'])} comments")
        if stats:
            line += f" The latest {kind} has {', '.join(stats)}"
            v = r.get("latest_vs_typical")
            if v is not None:
                if v >= 10:
                    line += f" — about {v} percent above your recent typical. Well done"
                elif v <= -10:
                    line += f" — about {-v} percent below your recent typical"
                else:
                    line += " — right around your recent typical"
            line += "."
    if r.get("engagement_rate") is not None:
        line += f" Engagement rate is {r['engagement_rate']} percent."
    dsp = r.get("days_since_post")
    if dsp is not None and dsp >= 4:
        line += (f" It has been {dsp} days since the last post, sir — "
                 f"consistency is the strongest growth lever.")
    return line


def spoken_report(handle: Optional[str] = None) -> str:
    """What WIVEXX says for 'how is my instagram doing?'."""
    trackers = list_trackers()
    if handle:
        t = find_tracker(handle)
        trackers = [t] if t else []
        if not trackers:
            return (f"I'm not tracking @{_clean_handle(handle)} yet, sir. "
                    f"Say 'track instagram {_clean_handle(handle)}' to begin.")
    if not trackers:
        return ("No social accounts are being tracked yet, sir. "
                "Say 'track instagram' followed by the handle, or add one "
                "from the Goals window.")
    lines = []
    for t in trackers:
        r = report(t["platform"], t["handle"])
        if r is None:
            lines.append(f"@{t['handle']}: "
                         + explain_fetch_error(t["handle"]).rstrip("."))
        elif r.get("follower_delta") is None:
            lines.append(_spoken_one(r)
                         + " Growth figures will appear once I have a few "
                           "days of history")
        else:
            lines.append(_spoken_one(r))
    return "Here is your social media briefing, sir. " + " ".join(
        l.rstrip(".") + "." for l in lines)


# ── Once-per-session briefing ──────────────────────────────────────────────────
def start_session_briefing(announce: Callable[[str], None], logger=None,
                           delay_secs: float = 90.0, speak: bool = True) -> None:
    """Once per WIVEXX session: snapshot every tracked account in the
    background.  When *speak* is True, also deliver ONE spoken briefing through
    the non-interrupting announcement queue.  When False, refresh the data
    SILENTLY (so the social cards/graphs stay current) without saying anything.
    Does nothing when no trackers exist."""
    global _logger
    _logger = logger or _logger

    def _run() -> None:
        global _briefed_this_session
        if _briefed_this_session or not list_trackers():
            return
        time.sleep(delay_secs)          # let boot/greeting finish first
        try:
            # Snapshot first; stay QUIET if no account has any data at all
            # (e.g. Instagram is rate-limiting and history is still empty) —
            # a failure announcement every boot would be noise.
            got_any = False
            for t in list_trackers():
                if report(t["platform"], t["handle"]) is not None:
                    got_any = True
            _briefed_this_session = True
            if not speak:
                if _logger:
                    _logger.info("socials: silent session refresh done "
                                 "(spoken briefing off)")
                return
            if not got_any:
                if _logger:
                    _logger.info("socials: briefing skipped — no data "
                                 "available yet")
                return
            announce(spoken_report())
            if _logger:
                _logger.info("socials: session briefing delivered")
        except Exception as e:
            if _logger:
                _logger.debug("socials briefing failed: %s", e)

    threading.Thread(target=_run, daemon=True,
                     name="wivexx-socials-briefing").start()
