#!/usr/bin/env python3
"""youtube_analytics.py — YouTube の分析の数字を、API で取って残す.

これまでは YouTube Studio の画面や書き出した zip から読んでいた数字を、自動で取る。
議論係（debate_runner.py）に渡して、AI が想像ではなく実際の数字で企画を議論できるようにする。

取るもの（期間は既定で直近90日）:
  - 動画ごとの表示回数・クリック率・視聴回数・平均視聴時間・平均視聴率・登録の増加
  - 本編ごとの視聴者維持率（動画の長さを100に分けた点ごと）と、30秒の時点の残り
  - 本編ごとの来た道（YouTube 検索・ブラウジング・関連動画・ショートなど）

置き場所:
  iCloud の PixelstoMotors/sns_analytics/youtube/api/
    {開始}_{終了}.json   取ったままの数字
    {開始}_{終了}.md     議論に渡す要約（本編の一覧・30秒の残り・来た道）

許可:
  ~/.config/pixelstomotors/client_secret.json             youtube_schedule.py と同じもの（pixelstomotors-studio）
  ~/.config/pixelstomotors/youtube_analytics_token.json   分析を読む許可（このスクリプトが作る。ほかの許可とは別のファイル）
  分析を読むだけ。動画を変えたり上げたりする権限はない

使い方（Mac のターミナルで）:
  python3 youtube_analytics.py --login                 最初に1回。ブラウザで Pixels to Motors を選んで許可
  python3 youtube_analytics.py                         直近90日を取る
  python3 youtube_analytics.py --days 28               直近28日
  python3 youtube_analytics.py --start 2026-10-01 --end 2026-11-13
"""
import argparse, json, os, sys, urllib.error, urllib.parse, urllib.request
from datetime import date, datetime, timedelta, timezone

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import youtube_schedule as ys   # ログインの仕組みは youtube_schedule.py のものを使う

# ブランドアカウントにはメールがないので openid・email は入れない（youtube_upload.py と同じ理由）
ys.SCOPE = "https://www.googleapis.com/auth/yt-analytics.readonly https://www.googleapis.com/auth/youtube.readonly"
ys.TOKEN = os.path.join(ys.CONF, "youtube_analytics_token.json")

JST = timezone(timedelta(hours=9))
AN = "https://youtubeanalytics.googleapis.com/v2/reports"
VIDEOS_JSON = os.path.join(HERE, "youtube_videos.json")          # youtube_schedule.py が作る動画の一覧
PTM = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))     # PixelstoMotors/
OUTDIR = os.path.join(PTM, "sns_analytics", "youtube", "api")

BASE = "views,estimatedMinutesWatched,averageViewDuration,averageViewPercentage,subscribersGained"
REACH = "videoThumbnailImpressions,videoThumbnailImpressionsClickRate"

TRAFFIC_JA = {
    "YT_SEARCH": "YouTube 検索", "SUBSCRIBER": "ブラウジング（登録者）", "RELATED_VIDEO": "関連動画",
    "YT_CHANNEL": "チャンネルページ", "EXT_URL": "外部", "SHORTS": "ショートのフィード",
    "NO_LINK_OTHER": "直接・不明", "PLAYLIST": "再生リスト", "END_SCREEN": "終了画面",
    "NOTIFICATION": "通知", "YT_OTHER_PAGE": "YouTube のほかのページ", "HASHTAGS": "ハッシュタグ",
    "BROWSE": "ブラウジング", "SHORTS_CONTENT_LINKS": "関連のショート動画", "ANNOTATION": "カード",
    "YT_PLAYLIST_PAGE": "再生リストのページ", "SOUND_PAGE": "サウンドのページ", "CAMPAIGN_CARD": "キャンペーン",
}


def get(tok, **q):
    url = AN + "?" + urllib.parse.urlencode({"ids": "channel==MINE", **q})
    req = urllib.request.Request(url, headers={"Authorization": "Bearer " + tok})
    try:
        with urllib.request.urlopen(req, timeout=120) as r:
            return json.loads(r.read().decode())
    except urllib.error.HTTPError as e:
        raise RuntimeError(f"分析の API がエラーを返しました（{e.code}）: {e.read().decode('utf-8', 'replace')[:600]}")


def rows(d):
    cols = [c["name"] for c in d.get("columnHeaders", [])]
    return [dict(zip(cols, r)) for r in d.get("rows", []) or []]


def video_list():
    """youtube_videos.json から、題名・長さ・ショートかどうかを引く"""
    try:
        d = json.load(open(VIDEOS_JSON, encoding="utf-8"))
        return {v["id"]: v for v in d.get("動画", [])}
    except Exception:
        return {}


def fmt_sec(s):
    s = int(round(s or 0))
    return f"{s // 60}分{s % 60:02d}秒"


def main():
    ap = argparse.ArgumentParser(description="YouTube の分析の数字を API で取る")
    ap.add_argument("--login", action="store_true")
    ap.add_argument("--days", type=int, default=90)
    ap.add_argument("--start")
    ap.add_argument("--end")
    a = ap.parse_args()

    if a.login:
        ys.login()
        tok = ys.access_token()
        me = ys.call(f"{ys.API}/channels?part=snippet&mine=true", headers={"Authorization": "Bearer " + tok})
        if ys.CHANNEL_ID not in [i["id"] for i in me.get("items", [])]:
            raise SystemExit("許可したアカウントのチャンネルが Pixels to Motors ではありません。--login で選び直してください")
        print("チャンネル Pixels to Motors の分析を読める許可です")
        return

    end = date.fromisoformat(a.end) if a.end else date.today() - timedelta(days=1)
    start = date.fromisoformat(a.start) if a.start else end - timedelta(days=a.days - 1)
    tok = ys.access_token()
    vids = video_list()
    print(f"期間: {start}〜{end}")

    # 1. 動画ごとの数字（表示回数とクリック率が取れないときは、それ抜きで取り直す）
    note = ""
    try:
        per = rows(get(tok, startDate=start, endDate=end, dimensions="video", metrics=BASE + "," + REACH,
                       sort="-views", maxResults=200))
    except RuntimeError as e:
        note = f"表示回数とクリック率は取れませんでした（{str(e)[:160]}）"
        print("注意:", note)
        per = rows(get(tok, startDate=start, endDate=end, dimensions="video", metrics=BASE, sort="-views", maxResults=200))
    for r in per:
        v = vids.get(r["video"], {})
        r["title"] = v.get("title", "（一覧にない動画）")
        r["seconds"] = v.get("seconds")
        r["is_short"] = v.get("is_short")
        r["published"] = v.get("published")
    longs = [r for r in per if r.get("is_short") is False]

    # 2. 本編ごとの視聴者維持率と来た道
    for r in longs:
        vid = r["video"]
        try:
            ret = rows(get(tok, startDate=start, endDate=end, dimensions="elapsedVideoTimeRatio",
                           metrics="audienceWatchRatio,relativeRetentionPerformance",
                           filters=f"video=={vid};audienceType==ORGANIC", sort="elapsedVideoTimeRatio"))
            r["retention"] = ret
            if ret and r.get("seconds"):
                x = 30 / r["seconds"]
                near = min(ret, key=lambda p: abs(p["elapsedVideoTimeRatio"] - x))
                r["retention_30s"] = near["audienceWatchRatio"]
        except RuntimeError as e:
            r["retention_error"] = str(e)[:200]
        try:
            r["traffic"] = rows(get(tok, startDate=start, endDate=end, dimensions="insightTrafficSourceType",
                                    metrics="views,averageViewDuration", filters=f"video=={vid}", sort="-views"))
        except RuntimeError as e:
            r["traffic_error"] = str(e)[:200]

    os.makedirs(OUTDIR, exist_ok=True)
    stem = f"{start}_{end}"
    with open(os.path.join(OUTDIR, stem + ".json"), "w", encoding="utf-8") as f:
        json.dump({"取った日時": datetime.now(JST).isoformat(timespec="minutes"), "期間": [str(start), str(end)],
                   "注意": note, "動画": per}, f, ensure_ascii=False, indent=1)

    # 3. 議論に渡す要約
    L = [f"# YouTube の数字（{start}〜{end}・API で取得 {datetime.now(JST).strftime('%Y-%m-%d %H:%M')}）", ""]
    if note:
        L += [f"- 注意: {note}", ""]
    tv = sum(r["views"] for r in per)
    lv = sum(r["views"] for r in longs)
    L += [f"- 視聴の合計 {tv:,} 回（本編 {lv:,} 回・ショート {tv - lv:,} 回）", "",
          "## 本編（視聴の多い順）", "",
          "| 題名 | 長さ | 表示 | クリック率 | 視聴 | 平均視聴 | 平均視聴率 | 30秒の残り | 登録 |",
          "|---|---|---|---|---|---|---|---|---|"]
    for r in sorted(longs, key=lambda r: -r["views"]):
        imp = r.get("videoThumbnailImpressions")
        ctr = r.get("videoThumbnailImpressionsClickRate")
        r30 = r.get("retention_30s")
        L.append(f"| {r['title'][:60]} | {fmt_sec(r.get('seconds'))} | {imp if imp is not None else '—'} | "
                 f"{f'{ctr * 100:.1f}%' if ctr is not None else '—'} | {r['views']} | {fmt_sec(r['averageViewDuration'])} | "
                 f"{r['averageViewPercentage']:.0f}% | {f'{r30 * 100:.0f}%' if r30 is not None else '—'} | {r['subscribersGained']} |")
    L += ["", "## 本編の来た道（上位3つ）", ""]
    for r in sorted(longs, key=lambda r: -r["views"]):
        t = r.get("traffic") or []
        if t:
            parts = [f"{TRAFFIC_JA.get(x['insightTrafficSourceType'], x['insightTrafficSourceType'])} {x['views']}" for x in t[:3]]
            L.append(f"- {r['title'][:50]}：{'・'.join(parts)}")
    L += ["", "## ショート（視聴の多い順・上位10）", "",
          "| 題名 | 視聴 | 平均視聴率 | 登録 |", "|---|---|---|---|"]
    for r in sorted([r for r in per if r.get("is_short")], key=lambda r: -r["views"])[:10]:
        L.append(f"| {r['title'][:60]} | {r['views']} | {r['averageViewPercentage']:.0f}% | {r['subscribersGained']} |")
    L += ["", "- 30秒の残り：維持率のグラフで、30秒の時点にいちばん近い点の値（自然な流入だけ。再生を始めた人に対する割合）",
          "- 平均視聴率：1回の再生で、動画の長さの何%を見たか", ""]
    with open(os.path.join(OUTDIR, stem + ".md"), "w", encoding="utf-8") as f:
        f.write("\n".join(L))
    print(f"書き出しました: sns_analytics/youtube/api/{stem}.json と .md（本編 {len(longs)} 本・全部で {len(per)} 本）")


if __name__ == "__main__":
    try:
        main()
    except RuntimeError as e:
        raise SystemExit(f"× {e}")
