#!/usr/bin/env python3
"""
SWAPA Video Analytics — Data Fetcher
Pulls channel + per-video stats from YouTube Data API v3 and the Vimeo API.
Outputs: data/video_stats.json  ← read by swapa_video_dashboard.html

Normal usage (runs via launchd every Monday at 9:15am):
  python3 fetch_video_stats.py
"""

import json
import re
import urllib.request
import urllib.parse
from datetime import datetime, timezone
from pathlib import Path

# ── CONFIG ────────────────────────────────────────────────────────────────────
YOUTUBE_API_KEY    = "AIzaSyA4spAzne6auHRLy4VZkiAcgWhQIPSaX1o"
YOUTUBE_CHANNEL_ID  = "UCv2ReHY98iAvmKTu31qJZTA"

# YouTube Analytics (OAuth) — watch-time data. Refresh token never expires unless
# revoked at myaccount.google.com/permissions. Re-run get_youtube_oauth_token.py
# if it ever needs replacing.
YOUTUBE_OAUTH_CLIENT_ID     = "1057465255011-947k71f2ijnlb1uehabc1ckqu2u4mbnq.apps.googleusercontent.com"
YOUTUBE_OAUTH_CLIENT_SECRET = "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"
YOUTUBE_OAUTH_REFRESH_TOKEN = "1//0fjMkyi21cBNHCgYIARAAGA8SNwF-L9Ir9KXOg4hH7Qaoh-wJcUQiwlf24cqwy2OgxAFPhoJkROD4iK1wsXR2h8nkw8iu8UxTP1Y"

VIMEO_ACCESS_TOKEN  = "ca0cf714f02fa851f408a6b031daea7b"

OUTPUT_DIR = Path(__file__).parent / "data"

# ── HELPERS ───────────────────────────────────────────────────────────────────
def http_get_json(url, headers=None):
    req = urllib.request.Request(url)
    req.add_header("User-Agent", "SWAPA-Analytics/1.0")
    for k, v in (headers or {}).items():
        req.add_header(k, v)
    with urllib.request.urlopen(req, timeout=30) as r:
        return json.loads(r.read())

def parse_iso8601_duration(s):
    """Convert YouTube's ISO 8601 duration (e.g. PT4M13S) to seconds."""
    if not s:
        return 0
    m = re.match(r"PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?", s)
    if not m:
        return 0
    h, mi, se = (int(g) if g else 0 for g in m.groups())
    return h * 3600 + mi * 60 + se

def chunked(seq, size):
    for i in range(0, len(seq), size):
        yield seq[i:i + size]

def http_post_json(url, data, headers=None):
    encoded = urllib.parse.urlencode(data).encode()
    req = urllib.request.Request(url, data=encoded)
    req.add_header("Content-Type", "application/x-www-form-urlencoded")
    for k, v in (headers or {}).items():
        req.add_header(k, v)
    with urllib.request.urlopen(req, timeout=30) as r:
        return json.loads(r.read())

# ── YOUTUBE ANALYTICS (watch time, OAuth) ─────────────────────────────────────
def get_youtube_analytics_access_token():
    resp = http_post_json("https://oauth2.googleapis.com/token", {
        "client_id":     YOUTUBE_OAUTH_CLIENT_ID,
        "client_secret": YOUTUBE_OAUTH_CLIENT_SECRET,
        "refresh_token": YOUTUBE_OAUTH_REFRESH_TOKEN,
        "grant_type":    "refresh_token",
    })
    token = resp.get("access_token")
    if not token:
        raise RuntimeError(f"YouTube Analytics auth failed: {resp}")
    return token

def fetch_youtube_channel_period_stats(access_token, start_date, end_date):
    """
    Channel-level totals for a date window, straight from YouTube Analytics.
    Unlike summing per-video stats, this correctly includes videos that are no
    longer public (private/unlisted/deleted) but that Analytics still tracks
    historically for the channel owner — those can be a large share of lifetime
    watch time and are invisible to the public Data API / uploads playlist.
    """
    params = urllib.parse.urlencode({
        "ids":       "channel==MINE",
        "startDate": start_date,
        "endDate":   end_date,
        "metrics":   "views,estimatedMinutesWatched,averageViewPercentage,subscribersGained",
    })
    resp = http_get_json(
        f"https://youtubeanalytics.googleapis.com/v2/reports?{params}",
        {"Authorization": f"Bearer {access_token}"},
    )
    rows = resp.get("rows", [])
    if not rows:
        return {"views": 0, "estimated_minutes_watched": 0, "avg_view_percentage": 0.0, "subscribers_gained": 0}
    views, minutes_watched, avg_pct, subs_gained = rows[0]
    return {
        "views":                    views,
        "estimated_minutes_watched": minutes_watched,
        "avg_view_percentage":      round(avg_pct, 1),
        "subscribers_gained":       subs_gained,
    }

def fetch_youtube_watch_time(access_token, video_ids):
    """
    Per-video watch-time stats via the YouTube Analytics API (requires OAuth,
    set up once via get_youtube_oauth_token.py). Returns dict keyed by video ID.
    Videos with no recorded analytics (e.g. very low views) are simply absent.
    Note: this only covers videos still in the public uploads playlist — see
    fetch_youtube_channel_period_stats() for accurate channel-wide totals.
    """
    print("Fetching YouTube watch-time data (OAuth)...")

    today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
    result = {}
    # The API accepts all video IDs in one filter list; batch defensively anyway
    # in case a channel grows past what a single request's URL length allows.
    for batch in chunked(video_ids, 300):
        params = urllib.parse.urlencode({
            "ids":        "channel==MINE",
            "startDate":  "2005-01-01",
            "endDate":    today,
            "metrics":    "views,estimatedMinutesWatched,averageViewDuration,averageViewPercentage",
            "dimensions": "video",
            "filters":    "video==" + ",".join(batch),
            "maxResults": 300,
        })
        resp = http_get_json(
            f"https://youtubeanalytics.googleapis.com/v2/reports?{params}",
            {"Authorization": f"Bearer {access_token}"},
        )
        for row in resp.get("rows", []):
            vid, _views, minutes_watched, avg_duration_sec, avg_pct = row
            result[vid] = {
                "estimated_minutes_watched": minutes_watched,
                "avg_view_duration_sec":     avg_duration_sec,
                "avg_view_percentage":       round(avg_pct, 1),
            }
    print(f"  → watch-time data for {len(result)}/{len(video_ids)} videos")
    return result

# ── YOUTUBE ───────────────────────────────────────────────────────────────────
def fetch_youtube():
    print("Fetching YouTube channel...")
    params = urllib.parse.urlencode({
        "part": "snippet,statistics,contentDetails",
        "id": YOUTUBE_CHANNEL_ID,
        "key": YOUTUBE_API_KEY,
    })
    resp = http_get_json(f"https://www.googleapis.com/youtube/v3/channels?{params}")
    items = resp.get("items", [])
    if not items:
        raise RuntimeError(f"YouTube channel not found: {resp}")
    ch = items[0]
    uploads_playlist = ch["contentDetails"]["relatedPlaylists"]["uploads"]
    channel_title = ch["snippet"]["title"]
    stats = ch["statistics"]
    print(f"  → {channel_title}: {stats.get('subscriberCount')} subs, {stats.get('videoCount')} videos")

    # Page through the uploads playlist to collect every video ID
    video_ids = []
    page_token = ""
    while True:
        params = urllib.parse.urlencode({
            "part": "contentDetails",
            "playlistId": uploads_playlist,
            "maxResults": 50,
            "key": YOUTUBE_API_KEY,
            **({"pageToken": page_token} if page_token else {}),
        })
        resp = http_get_json(f"https://www.googleapis.com/youtube/v3/playlistItems?{params}")
        video_ids.extend(item["contentDetails"]["videoId"] for item in resp.get("items", []))
        page_token = resp.get("nextPageToken")
        if not page_token:
            break
    print(f"  → {len(video_ids)} video IDs collected")

    # Batch-fetch stats for every video, 50 IDs at a time (API limit)
    videos = []
    for batch in chunked(video_ids, 50):
        params = urllib.parse.urlencode({
            "part": "snippet,statistics,contentDetails",
            "id": ",".join(batch),
            "key": YOUTUBE_API_KEY,
        })
        resp = http_get_json(f"https://www.googleapis.com/youtube/v3/videos?{params}")
        for v in resp.get("items", []):
            vstats = v.get("statistics", {})
            videos.append({
                "id":            v["id"],
                "title":         v["snippet"]["title"],
                "published_at":  v["snippet"]["publishedAt"][:10],
                "views":         int(vstats.get("viewCount", 0)),
                "likes":         int(vstats.get("likeCount", 0)),
                "comments":      int(vstats.get("commentCount", 0)),
                "duration_sec":  parse_iso8601_duration(v["contentDetails"]["duration"]),
                "url":           f"https://www.youtube.com/watch?v={v['id']}",
            })
    print(f"  → {len(videos)} videos fetched")

    # Watch-time data (OAuth). Best-effort — dashboard still works without it.
    watch_time_available = False
    period_stats = None
    try:
        access_token = get_youtube_analytics_access_token()

        watch_time = fetch_youtube_watch_time(access_token, video_ids)
        for v in videos:
            wt = watch_time.get(v["id"])
            if wt:
                v.update(wt)
                watch_time_available = True

        print("Fetching YouTube channel-level totals (lifetime + this year)...")
        today = datetime.now(timezone.utc)
        period_stats = {
            "lifetime":  fetch_youtube_channel_period_stats(access_token, "2005-01-01", today.strftime("%Y-%m-%d")),
            "this_year": fetch_youtube_channel_period_stats(access_token, f"{today.year}-01-01", today.strftime("%Y-%m-%d")),
        }
        print(f"  → lifetime: {period_stats['lifetime']['views']:,} views, "
              f"{period_stats['lifetime']['estimated_minutes_watched']:,} min watched")
        print(f"  → this year: {period_stats['this_year']['views']:,} views, "
              f"{period_stats['this_year']['estimated_minutes_watched']:,} min watched")
    except Exception as e:
        print(f"  ⚠ YouTube Analytics error: {e}")
        print("    Watch-time and period-stats fields will be omitted this run.")

    videos.sort(key=lambda v: v["views"], reverse=True)

    return {
        "channel_title":         channel_title,
        "channel_url":           f"https://www.youtube.com/channel/{YOUTUBE_CHANNEL_ID}",
        "subscriber_count":      int(stats.get("subscriberCount", 0)),
        "view_count":            int(stats.get("viewCount", 0)),
        "video_count":           int(stats.get("videoCount", 0)),
        "watch_time_available":  watch_time_available,
        "period_stats":          period_stats,
        "videos":                videos,
    }

# ── VIMEO ─────────────────────────────────────────────────────────────────────
def fetch_vimeo():
    print("Fetching Vimeo account...")
    params = urllib.parse.urlencode({
        "fields": "name,link,metadata.connections.followers.total,metadata.connections.videos.total",
    })
    me = http_get_json(
        f"https://api.vimeo.com/me?{params}",
        {"Authorization": f"bearer {VIMEO_ACCESS_TOKEN}"},
    )
    account_name = me.get("name", "")
    followers = me.get("metadata", {}).get("connections", {}).get("followers", {}).get("total", 0)
    print(f"  → {account_name}: {followers} followers")

    videos = []
    page = 1
    fields = ",".join([
        "uri", "name", "link", "created_time", "duration",
        "stats.plays",
        "metadata.connections.likes.total",
        "metadata.connections.comments.total",
    ])
    while True:
        params = urllib.parse.urlencode({
            "per_page": 100,
            "page": page,
            "fields": fields,
        })
        resp = http_get_json(
            f"https://api.vimeo.com/me/videos?{params}",
            {"Authorization": f"bearer {VIMEO_ACCESS_TOKEN}"},
        )
        batch = resp.get("data", [])
        for v in batch:
            conns = v.get("metadata", {}).get("connections", {})
            videos.append({
                "id":           v["uri"].split("/")[-1],
                "title":        v.get("name", ""),
                "published_at": (v.get("created_time") or "")[:10],
                "plays":        v.get("stats", {}).get("plays") or 0,
                "likes":        conns.get("likes", {}).get("total", 0),
                "comments":     conns.get("comments", {}).get("total", 0),
                "duration_sec": v.get("duration", 0),
                "url":          v.get("link", ""),
            })
        if not resp.get("paging", {}).get("next"):
            break
        page += 1
    videos.sort(key=lambda v: v["plays"], reverse=True)
    print(f"  → {len(videos)} videos fetched")

    return {
        "account_name": account_name,
        "account_url":  me.get("link", ""),
        "follower_count": followers,
        "video_count":  len(videos),
        "total_plays":  sum(v["plays"] for v in videos),
        "videos":       videos,
    }

# ── OUTPUT ────────────────────────────────────────────────────────────────────
def write_output(youtube_data, vimeo_data):
    OUTPUT_DIR.mkdir(exist_ok=True)
    out_path = OUTPUT_DIR / "video_stats.json"

    payload = {
        "generated_at": datetime.now(timezone.utc).isoformat(),
        "youtube":      youtube_data,
        "vimeo":        vimeo_data,
    }

    with open(out_path, "w") as f:
        json.dump(payload, f, indent=2)

    print(f"\n✓ Written to {out_path}")
    print(f"  YouTube videos: {youtube_data['video_count']}  (subs: {youtube_data['subscriber_count']})")
    if youtube_data["period_stats"]:
        print(f"  YouTube lifetime watch time: {youtube_data['period_stats']['lifetime']['estimated_minutes_watched']:,} min")
    else:
        print("  YouTube watch time: not available this run")
    print(f"  Vimeo videos:   {vimeo_data['video_count']}  (followers: {vimeo_data['follower_count']})")

# ── MAIN ──────────────────────────────────────────────────────────────────────
def main():
    youtube_data = fetch_youtube()
    vimeo_data = fetch_vimeo()
    write_output(youtube_data, vimeo_data)

if __name__ == "__main__":
    main()
