#!/usr/bin/env python3
"""sync_youtube_shorts.py — YouTube で公開予約したショートと本編の X 投稿を、公開の時刻に合わせて予定表に入れる。

元になるもの:
  sns/SHORT_ep{N}_sns_posts.json   ショートの X の文（posts.x_template。{url} がショートの URL に替わる）
  sns/VIDEO_{名前}_sns_posts.json  本編の X の文（2026-10-24 から。{url} が https://youtu.be/{id} に替わる）。
                                   動画との結びつけは、タイトルが meta.youtube_title と同じもの、
                                   なければ meta.title_match の語をすべて含み、長さが meta.seconds と3秒以内のもの
                                   （YouTube に上げるときにタイトルを直しても結びつくように）
  youtube_videos.json              エイイチの Mac が1時間ごとに書き出す自分のチャンネルの一覧
                                   （_制作ベース/youtube_schedule.py。説明文の manga/ep{N} から manga_ep を拾う）

やること:
  1. ショートは manga_ep（なければタイトル）、本編はタイトル（なければ語と長さ）で、json と YouTube の動画を結びつける
  2. 公開予約（または公開済み）の時刻を X の予約の時刻にする。同じ時刻に別の X の予約があれば、
     19:30 なら 20:00、それ以外は30分ずつ後ろへずらす（2026-10-24 エイイチの指示「19:30が重なったら動画は20:00」）
     12:30 の枠（2026-10-25 に足した）は「それ以外」に入り、13:00 になる
  3. まだ投稿していない予約は、YouTube 側で時刻を変えていたら合わせて直す
  4. 公開の時刻を過ぎていて、まだ予定表にないものは入れない（古いものを今さら出さない）
LinkedIn には入れない（動画は LinkedIn に直接アップロードする決まり。video_rules.md の 0節の22）。

使い方: python3 tools/sync_youtube_shorts.py --videos youtube_videos.json [--dry-run]
"""
import argparse, glob, json, os, re, sys
from datetime import datetime, timedelta, timezone

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
JST = timezone(timedelta(hours=9))
SCHEDULE = os.path.join(ROOT, "sns", "schedule.json")
POSTED = os.path.join(ROOT, "sns", "posted.json")


def norm(t):
    return re.sub(r"\s+", "", re.split(r"\s#", t or "")[0])


def load(p, d):
    try:
        return json.load(open(p, encoding="utf-8"))
    except FileNotFoundError:
        return d


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--videos", required=True)
    ap.add_argument("--dry-run", action="store_true")
    a = ap.parse_args()
    allv = load(a.videos, {}).get("動画", [])
    vids = [v for v in allv if v.get("is_short")]
    mains = [v for v in allv if not v.get("is_short")]
    sched = load(SCHEDULE, {"posts": []})
    posted = load(POSTED, {})
    done_ids = set(posted.keys()) if isinstance(posted, dict) else {p.get("id") for p in posted.get("posts", posted) if isinstance(p, dict)}
    now = datetime.now(timezone.utc)
    changes = []
    paths = sorted(glob.glob(os.path.join(ROOT, "sns", "SHORT_ep*_sns_posts.json"))) + \
        sorted(glob.glob(os.path.join(ROOT, "sns", "VIDEO_*_sns_posts.json")))
    for path in paths:
        src = json.load(open(path, encoding="utf-8"))
        meta, posts = src["meta"], src["posts"]
        is_main = os.path.basename(path).startswith("VIDEO_")
        if is_main:
            words = meta.get("title_match") or []
            cand = [v for v in mains if norm(v["title"]) == norm(meta.get("youtube_title"))] or \
                [v for v in mains if words and all(w in v["title"] for w in words)
                 and meta.get("seconds") and v.get("seconds") and abs(v["seconds"] - meta["seconds"]) <= 3]
            sid = os.path.basename(path)[:-len("_sns_posts.json")] + "-x"
        else:
            ep = meta["episode"]
            cand = [v for v in vids if v.get("manga_ep") == ep] or [v for v in vids if norm(v["title"]) == norm(meta.get("youtube_title"))]
            sid = f"SHORT_ep{ep}-x"
        cand = [v for v in cand if v.get("publishAt") or v.get("published")]
        if not cand:
            continue
        v = sorted(cand, key=lambda v: v.get("publishAt") or v.get("published"))[-1]
        when = datetime.fromisoformat((v.get("publishAt") or v.get("published")).replace("Z", "+00:00"))
        entry = next((e for e in sched["posts"] if e["id"] == sid), None)
        if entry and sid in done_ids:
            continue
        if not entry and when < now:
            continue
        url = f"https://youtu.be/{v['id']}" if is_main else f"https://youtube.com/shorts/{v['id']}"
        text = posts["x_template"].replace("{url}", url)
        if posts.get("x") != text or meta.get("url") != url or meta.get("youtube_id") != v["id"]:
            posts["x"], meta["url"], meta["youtube_id"] = text, url, v["id"]
            if not a.dry_run:
                with open(path, "w", encoding="utf-8") as f:
                    json.dump(src, f, ensure_ascii=False, indent=2); f.write("\n")
        # 同じ時刻の X の予約を避ける
        taken = {e["post_at"] for e in sched["posts"] if e["platform"] == "x" and e["id"] != sid}
        t = when.astimezone(JST)
        while t.isoformat() in taken:
            t = t.replace(hour=20, minute=0) if (t.hour, t.minute) == (19, 30) else t + timedelta(minutes=30)
        post_at = t.isoformat()
        if entry is None:
            sched["posts"].append({"id": sid, "source": os.path.relpath(path, ROOT), "platform": "x", "post_at": post_at})
            changes.append(f"足した {sid} {post_at} {v['title'][:30]}")
        elif entry["post_at"] != post_at:
            changes.append(f"時刻を直した {sid} {entry['post_at']} → {post_at}")
            entry["post_at"] = post_at
    sched["posts"].sort(key=lambda e: (e["post_at"], e["id"]))
    if changes and not a.dry_run:
        with open(SCHEDULE, "w", encoding="utf-8") as f:
            json.dump(sched, f, ensure_ascii=False, indent=2); f.write("\n")
    print("\n".join(changes) or "変わったものはありません")
    return 0


if __name__ == "__main__":
    sys.exit(main())
