#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""IG StudioSoft — REEL 2x/hari (11:00 & 19:00 WIB) dengan BACKGROUND BERGANTIAN putih/biru.

Perintah Bos 30 Sep 2026 (22:55 WIB): *"untuk IG StudioSoft harus bergantian warna Backgroundnya ..
untuk Reels ini bergantian BG nya . untuk Reels sehari tayang 2 postingan Reels jam 11.00 dan jam 19.00.
Besok jam 11.00 BG : Putih dan jam 19 BG Biru. besoknya jam 11.00 BG Biru dan jam 19. BG Putih ..
begitu terus ( bergantian warna Backgroundnya )"*

Aturan yang diterapkan (anchor DIPERBAIKI 3 Okt 2026 — perintah Bos: *"Ini harusnya Background putih.
Ayo pasang 18 bg putih. Coba kamu pelajari Ritmenya"*):
  • 2 reel/hari: **11:00 WIB** dan **19:00 WIB**.
  • Urutan warna SATU HARI (satu sumber kebenaran `ss_carousel_lib.warna_ke(tgl, urutan)`), jangan
    dihitung sendiri-sendiri per pipeline:
        urutan 1 = carousel feed 08:30   ·   urutan 2 = reel 11:00   ·   urutan 3 = reel 19:00
    Ganjil: carousel BIRU  → 11:00 PUTIH → 19:00 BIRU
    Genap : carousel PUTIH → 11:00 BIRU  → 19:00 PUTIH
    Ritmenya: tiap post yang tayang selalu kebalikan dari post sebelumnya di feed IG SS.
  • Patokan 2 Okt (reel 11:00 dihitung sebagai "urutan 1", pasangan baru per hari) bikin reel 11:00
    kembar dengan carousel 08:30 → 2-3 Okt dua BIRU berurutan di feed. Itu yang dikoreksi 3 Okt.
  • Sumber = album carousel Threads StudioSoft yang sudah ada (`ss_opt2_*`, 4 slide, 1080x1350):
      BIRU  = 05, 07, 09, 11, 13, 17, 19   (bg #122B63, teks putih)
      PUTIH = 06, 08, 10, 14, 16, 18, 20   (bg #FFFFFF, teks #122B63)
    TIDAK ada desain baru. Video = slide utuh di tengah **kanvas sewarna slide** (putih / #122B63),
    jadi tepinya menyatu; bukan latar blur seperti pipeline navy lama.
  • Rotasi: album warna itu yang belum pernah dipakai jadi reel IG SS; kalau 7 album habis → ulang dari awal.
  • Idempoten per slot (akun + scheduleAt). Story mirror feed ditangani `ss_ig_story_mirror.py` (+10 menit).
  • Jadwal lama diselaraskan dengan `atur_pola_reel.py` (tukar `scheduleAt` 11:00 ↔ 19:00 per hari).

Pakai:
  python3 ss_ig_reel_warna.py --hari 4            # dry-run rencana
  python3 ss_ig_reel_warna.py --hari 4 --go       # rakit video + create jadwal
  python3 ss_ig_reel_warna.py --seri 11 --jam 19 --tgl 2026-10-02 --go   # satu slot manual
"""
import argparse
import datetime
import os
import re
import subprocess
import sys
import time

WS = "/home/mantapman/.hermes/profiles/atun/workspace"
IMG = f"{WS}/openclaw-img"
MUSIK = f"{WS}/musik"
# Bank bed instrumental buatan sendiri (bebas royalti) — lihat musik_bank.py.
# Perintah Bos 20 Okt 2026: "pastikan semua postingan Instagram StudioSoft pakai lagu".
# Reels dibakar musiknya ke MP4 (IG Graph API tidak punya opsi musik), dipilih bergiliran
# supaya reel tidak terdengar sama terus. Semua bed 24 dtk, loopable, puncak -2.7 dB.
BED = [
    "kalem_hangat_24s.wav", "ceria_ringan_24s.wav", "kalem_lembut_24s.wav",
    "ceria_cerah_24s.wav", "kalem_malam_24s.wav", "ceria_jalan_24s.wav",
]
VOL_MUSIK = 0.80   # puncak hasil campur ≈ -4.6 dB (musik satu-satunya audio)
sys.path.insert(0, WS)
from repliz_client import get_client  # noqa: E402

IG_SS = "6a38ac33c5ff4ce3a3231232"
BASE = "https://mantapserver.tail3947bb.ts.net/openclaw-img/"
INGRESS = ["103.84.155.153", "103.84.155.217"]
WIB = datetime.timezone(datetime.timedelta(hours=7))
SLOT = [11]
TOPIK = "IG StudioSoft Reel Harian"
BIRU = (18, 43, 99)        # #122B63 — sewarna bg slide album biru
PUTIH = (255, 255, 255)

CTA = "Bagian mana yang paling relevan buat bisnismu? Cerita di komentar! 👇"
HASHTAG = "#studiosoft #digitalmarketing #seo #bisnissurabaya #umkm"

# album -> (judul, warna, link artikel hidup, penjabaran untuk caption)
ALBUM = {
    "05": ("Kecepatan website & konversi", "biru",
           "https://studiosoft.id/core-web-vitals-kecepatan-website-2026/",
           "kecepatan itu penentu. Loading lambat bikin 53% pengunjung pergi, dan Google memakai "
           "kecepatan sebagai sinyal peringkat. Kompres gambar, aktifkan cache, kurangi script berat, "
           "lalu cek skornya di PageSpeed Insights."),
    "06": ("Riset keyword pemula", "putih",
           "https://studiosoft.id/7-cara-menggunakan-ai-untuk-riset-kata-kunci-yang-lebih-cepat-dan-akurat/",
           "riset keyword bukan menebak. Lihat apa yang orang ketik, pilih long-tail yang kompetisinya "
           "kecil, sesuaikan dengan niat beli, dan tools gratis sudah cukup untuk mulai."),
    "07": ("On-page vs off-page", "biru",
           "https://studiosoft.id/apa-itu-seo-pengertian-cara-kerja-manfaat/",
           "on-page itu yang ada di dalam situs (judul, struktur, kecepatan), off-page yang dibangun "
           "di luar (backlink, mention, ulasan). Dua-duanya memengaruhi peringkat, jadi jangan pilih "
           "salah satu."),
    "08": ("Backlink berkualitas", "putih",
           "https://studiosoft.id/apa-itu-seo-pengertian-cara-kerja-manfaat/",
           "backlink itu rekomendasi digital. Satu rujukan dari situs yang dipercaya lebih berguna "
           "daripada sepuluh tautan asal-asalan."),
    "09": ("SEO untuk UMKM", "biru",
           "https://studiosoft.id/jasa-seo-sidoarjo/",
           "modal kecil bukan halangan. Bersaing di lingkup lokal dulu, tulis sepuluh artikel yang "
           "benar-benar dibutuhkan pelanggan, minta ulasan, lalu konsisten tiga bulan."),
    "10": ("SEO lokal Surabaya", "putih",
           "https://studiosoft.id/strategi-seo-lokal-2026-cara-praktis-mendatangkan-lebih-banyak-pelanggan-di-sekitar-bisnis-anda/",
           "kata kunci plus nama kota, Google Business Profile yang aktif, ulasan warga sekitar, dan "
           "konsistensi nama-alamat-telepon di semua platform itu yang bikin bisnis Anda muncul saat "
           "orang mengetik \"dekat saya\"."),
    # "11" DIHAPUS 6 Okt 2026 — perintah Bos: "Konten klinik keluarkan dari IG StudioSoft".
    # Album ini bertema Google Business Profile klinik (dokter, pasien, jam praktik), jadi tidak
    # boleh lagi dipakai sebagai reel IG StudioSoft. Rotasi reel otomatis melewatinya.
    "13": ("Strategi digital marketing 2026", "biru",
           "https://studiosoft.id/digital-marketing-surabaya-strategi-jitu-bisnis-naik-kelas/",
           "pergeserannya jelas: iklan makin mahal per klik, trust jadi mata uang utama, dan "
           "pelanggan benci konten gimmick. Fokuskan ke konten edukatif, data milik sendiri, dan "
           "komunitas kecil yang aktif."),
    "14": ("Manfaat digital marketing untuk UMKM", "putih",
           "https://studiosoft.id/digital-marketing-sidoarjo/",
           "digital marketing memangkas biaya promosi, membuka pasar lebih luas, dan membuat hasilnya "
           "terukur — tiga hal yang paling dicari pemilik usaha kecil."),
    "16": ("Landing page bisnis kecil", "putih",
           "https://studiosoft.id/apa-itu-seo-pengertian-cara-kerja-manfaat/",
           "landing page yang baik fokus pada satu tujuan, memuat cepat di HP, dan menuntun pengunjung "
           "ke satu aksi saja."),
    "17": ("Psikologi konversi", "biru",
           "https://studiosoft.id/7-alasan-landing-page-penting-untuk-tingkatkan-konversi-bisnis-online/",
           "yang mengubah pengunjung jadi pembeli biasanya sederhana: judul yang bicara ke masalah "
           "pembaca, manfaat yang konkret, CTA di beberapa titik, dan bukti sosial yang nyata."),
    "18": ("Konten IG yang dilirik", "putih",
           "https://studiosoft.id/paket-kelola-sosial-media-bulanan/",
           "konten yang dilihat orang biasanya menjawab satu pertanyaan spesifik, bukan sekadar "
           "menampilkan produk."),
    "19": ("Jenis konten sosmed", "biru",
           "https://studiosoft.id/paket-kelola-sosial-media-bulanan/",
           "campur proporsional: 40% edukasi dan tips, 30% proses serta cerita, 20% hiburan ringan, "
           "10% promosi langsung. Satu rasa terus-terusan bikin audiens bosan."),
    "20": ("Budget medsos UMKM", "putih",
           "https://studiosoft.id/jasa-google-ads-surabaya/",
           "budget media sosial tidak harus besar. Yang penting tahu mana yang mendatangkan pelanggan, "
           "bukan mana yang paling ramai."),
}

RE_CJK = re.compile(r"[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]")
RE_MENTION = re.compile(r"@[A-Za-z0-9_.]+")
RE_CTA = re.compile(r"komentar|pendapat|diskusi|setuju|share|pengalaman", re.I)


def warna_hari(tgl, jam):
    """Warna BG reel untuk slot `jam` — lanjutan urutan warna hari itu (perintah Bos 3 Okt 2026).

    Urutan warna satu hari (sumber kebenaran `ss_carousel_lib.warna_ke`, jangan dihitung ulang):
      urutan 1 = carousel feed 08:30 · urutan 2 = reel 11:00 · urutan 3 = reel 19:00.
    Jadi reels BUKAN pasangan baru per hari, tapi kelanjutan warna carousel:
      3 Okt (ganjil): carousel BIRU → 11:00 PUTIH → 19:00 BIRU
      4 Okt (genap) : carousel PUTIH → 11:00 BIRU → 19:00 PUTIH
    Ritmenya: tiap post yang tayang selalu kebalikan dari post sebelumnya di feed IG SS.
    """
    import ss_carousel_lib as L
    return L.warna_ke(tgl, 1 if jam == 19 else 2)


def seri_album(num, warna):
    return f"ss_opt2_{num}"


def video_path(num, warna):
    return f"{IMG}/ss_reel_{warna}_{num}.mp4"


def video_url(num, warna):
    return BASE + os.path.basename(video_path(num, warna))


def slide_files(num):
    out = []
    for f in sorted(os.listdir(IMG)):
        if re.match(rf"ss_opt2_{num}_\d\d\.jpg$", f):
            out.append(f"{IMG}/{f}")
    return out


def bed_untuk(num, warna):
    """Bed musik bergiliran per album/warna — biar tidak monoton antar reel.

    Rumus sengaja campur kalem & ceria (indeks BED bergantian), bukan satu rasa terus:
    reel edukasi lebih enak dengan kalem, reel tips dengan ceria.
    """
    i = ((int(num) + (0 if warna == "biru" else 2)) // 2) % len(BED)
    return os.path.join(MUSIK, BED[i])


def audio_terisi(path):
    """True kalau audio di dalam video tidak senyap (puncak > -50 dB)."""
    p = subprocess.run(["ffmpeg", "-hide_banner", "-i", path, "-af", "volumedetect",
                        "-f", "null", "-"], capture_output=True, text=True)
    m = re.search(r"max_volume: (-?[\d.]+) dB", p.stderr)
    return bool(m) and float(m.group(1)) > -50


def build(files, out, bg, dur=4.0, fade=0.35, fade_awal=False, musik=None):
    """Rakit 1080x1920 dari slide 4:5 di atas kanvas sewarna slide + bed musik.

    `fade_awal=False` (default, perintah Bos 1 Okt 2026): slide PERTAMA tampil utuh sejak
    frame 0 — tidak ada transisi fade di awal video. Slide berikutnya tetap pakai fade.
    `musik` = path WAV bed; dibakar ke MP4 (looping, fade in/out) karena IG hanya memutar
    audio yang ada di dalam berkas video.
    """
    W, H = 1080, 1920
    col = "white" if bg == PUTIH else "0x%02X%02X%02X" % bg
    n = len(files)
    total = dur * n
    cmd = ["ffmpeg", "-y", "-v", "error"]
    for f in files:
        cmd += ["-loop", "1", "-t", str(dur), "-i", f]
    parts, labels = [], []
    for i in range(n):
        efek = f"fade=t=out:st={dur - fade}:d={fade}:color={col}"
        if i > 0 or fade_awal:
            efek = f"fade=t=in:st=0:d={fade}:color={col}," + efek
        parts.append(
            f"[{i}:v]scale={W}:-2,pad={W}:{H}:0:(oh-ih)/2:color={col},"
            f"{efek},setsar=1[v{i}]"
        )
        labels.append(f"[v{i}]")
    fc = ";".join(parts) + ";" + "".join(labels) + f"concat=n={n}:v=1:a=0[vout]"
    if musik:
        cmd += ["-stream_loop", "-1", "-i", musik]
        fc += (f";[{n}:a]volume={VOL_MUSIK},afade=t=in:st=0:d=0.8,"
               f"afade=t=out:st={max(0.1, total - 1.6):.2f}:d=1.6,"
               f"atrim=0:{total:.2f},asetpts=N/SR/TB[aud]")
        peta_a = "[aud]"
        bit = "192k"
    else:
        cmd += ["-f", "lavfi", "-t", str(total), "-i", "anullsrc=r=44100:cl=stereo"]
        peta_a = f"{n}:a"
        bit = "128k"
    cmd += [
        "-filter_complex", fc, "-map", "[vout]", "-map", peta_a,
        "-c:v", "libx264", "-preset", "medium", "-crf", "22",
        "-pix_fmt", "yuv420p", "-profile:v", "high", "-level", "4.0",
        "-r", "30", "-c:a", "aac", "-b:a", bit, "-ar", "44100", "-shortest",
        "-movflags", "+faststart", out,
    ]
    print(f"    rakit {os.path.basename(out)} ({n} slide x {dur} dtk, kanvas {col}, "
          f"musik {os.path.basename(musik) if musik else '-'})")
    subprocess.run(cmd, check=True)
    return out


def rakit(num, warna, force=False):
    """Rakit reel satu album. Video lama tanpa musik otomatis dirakit ulang."""
    out = video_path(num, warna)
    if os.path.exists(out) and os.path.getsize(out) > 100_000 and not force:
        if audio_terisi(out):
            return out
        print(f"    ↻ {os.path.basename(out)} masih senyap — rakit ulang + musik")
    files = slide_files(num)
    if not files:
        print(f"    ⚠️ ss_opt2_{num}: file slide tidak ada — SKIP")
        return None
    bg = PUTIH if warna == "putih" else BIRU
    return build(files, out, bg, musik=bed_untuk(num, warna))


def caption(num):
    judul, warna, link, jabaran = ALBUM[num]
    # Huruf besar di awal diturunkan kecuali kata pertamanya akronim (SEO, UMKM) supaya
    # kalimatnya enak dibaca tanpa merusak singkatan.
    kata1 = judul.split()[0]
    jl = judul if kata1.isupper() else judul[0].lower() + judul[1:]
    # Perintah Bos 6 Okt 2026: caption reel jangan seragam. Lima bentuk pembuka berbeda,
    # dipilih dari nomor album supaya tidak pernah sama persis antar reel.
    PEMBUKA = [
        f"Klien sering tanya soal {jl}. Ini ringkasnya.",
        f"{judul}: tiga hal yang paling sering menentukan hasilnya.",
        f"Kalau {jl} masih terasa kabur, mulai dari sini.",
        f"Kami tulis ulang {jl} jadi versi pendek.",
        f"Pertanyaan yang muncul hampir tiap kali kami membahas {jl}.",
    ]
    LANJUTAN = [
        "Yang perlu diingat: kerjakan bagian yang paling dekat dengan pelanggan lebih dulu, ukur hasilnya, baru lanjut ke hal berikutnya.",
        "Urutannya berpengaruh. Mulai dari yang paling dekat dengan pembeli, ukur, lalu jaga konsistensinya tiap minggu.",
        "Tidak perlu dikerjakan sekaligus. Satu langkah rapi lebih berguna daripada lima langkah setengah jalan.",
        "Dasarnya sederhana: perbaiki yang sudah dikunjungi orang, ukur, ulangi.",
        "Yang membedakan hasil biasanya bukan alatnya, tapi apakah pekerjaannya dijalankan rutin.",
    ]
    PENUTUP = [
        "Bagian mana yang paling relevan buat bisnismu? Cerita di komentar.",
        "Kalau ada bagian yang ingin dibahas lebih panjang, tulis di komentar.",
        "Yang mana sudah kamu kerjakan? Tulis di komentar.",
        "Ada pertanyaan soal ini? Tanya saja di komentar.",
        "Bagian yang mana yang paling susah dijalankan? Cerita di komentar.",
    ]
    i = int(num)
    return (
        f"{PEMBUKA[i % len(PEMBUKA)]}\n\n"
        f"{jabaran}\n\n"
        f"{LANJUTAN[i % len(LANJUTAN)]}\n\n"
        f"Versi lengkapnya ada di artikel kami: {link}\n\n"
        f"{PENUTUP[i % len(PENUTUP)]}\n\n{HASHTAG}"
    )


def link_hidup(u):
    p = subprocess.run(["curl", "-4", "-s", "-o", "/dev/null", "-w", "%{http_code}",
                        "--max-time", "30", "-L", u], capture_output=True, text=True)
    return p.stdout.strip() == "200"


def valid(cap, cek_link=True):
    masalah = []
    if len(cap) > 2200:
        masalah.append(f"caption {len(cap)} char (> 2200)")
    if "studiosoft.id" not in cap:
        masalah.append("tanpa link studiosoft.id")
    if not RE_CTA.search(cap):
        masalah.append("tanpa ajakan komentar")
    if RE_CJK.search(cap):
        masalah.append("mengandung teks CJK")
    m = RE_MENTION.search(cap)
    if m:
        masalah.append(f"mention internal {m.group(0)}")
    if cek_link:
        for u in re.findall(r"https?://studiosoft\.id[^\s)]*", cap):
            if not link_hidup(u.rstrip(".,")):
                masalah.append(f"link mati: {u}")
    return masalah


def iso_utc(tgl, jam):
    return (datetime.datetime(tgl.year, tgl.month, tgl.day, jam, tzinfo=WIB)
            .astimezone(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z"))


def semua_jadwal(c):
    docs, page = [], 1
    while page <= 200:
        r = None
        for a in range(6):
            try:
                r = c.get_schedules(page=page, limit=100)
                break
            except Exception:
                time.sleep(5 + 3 * a)
        if r is None:
            raise SystemExit("gagal membaca jadwal Repliz")
        b = r.get("docs") or []
        if not b:
            break
        docs += b
        if len(b) < 100:
            break
        page += 1
    return docs


def cek_url_ingress(u):
    ok = True
    for ip in INGRESS:
        p = subprocess.run(["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "--max-time", "30",
                            "--resolve", f"mantapserver.tail3947bb.ts.net:443:{ip}", u],
                           capture_output=True, text=True)
        kode = p.stdout.strip()
        print(f"      {ip}: {kode}")
        ok = ok and kode == "200"
    return ok


def pakai(c, num, warna, at, cap, media, go):
    """Create + verifikasi satu slot reel."""
    if not go:
        return True
    sid = None
    for attempt in range(1, 4):
        try:
            res = c.create_schedule(IG_SS, cap, at,
                                    title=f"Reel IG SS {warna} ss_opt2_{num}",
                                    topic=TOPIK, type_="video", medias=media, replies=[],
                                    additional_info={"isShareToFeed": True})
            sid = res.get("scheduleId") or res.get("_id") or res.get("id")
            break
        except Exception as e:
            print(f"      ⚠️ percobaan {attempt}: {str(e)[:120]}")
            time.sleep(8)
    if not sid:
        print("      ❌ create gagal")
        return False
    d = c.get_schedule(sid)
    d = d.get("data") or d
    if isinstance(d, dict) and "docs" in d:
        d = d["docs"][0]
    ok = d.get("status") == "pending" and d.get("type") == "video"
    print(f"      {'✅' if ok else '⚠️'} id={sid} status={d.get('status')} at={d.get('scheduleAt')} "
          f"media={(d.get('medias') or [{}])[0].get('url', '').split('/')[-1]}")
    return ok


def album_terpakai(docs):
    terpakai, ada = {"biru": set(), "putih": set()}, set()
    for d in docs:
        if d.get("accountId") != IG_SS:
            continue
        if str(d.get("type")).lower() != "story":
            ada.add(d.get("scheduleAt"))
        for m in (d.get("medias") or []):
            mm = re.match(r"ss_reel_(biru|putih)_(\d\d)\.mp4$", (m.get("url") or "").split("/")[-1])
            if mm:
                terpakai[mm.group(1)].add(mm.group(2))
    return terpakai, ada


def roll(c, hari, go, force_video=False, cek_link=True):
    hari_ini = datetime.datetime.now(WIB).date()
    sekarang = datetime.datetime.now(datetime.timezone.utc)
    docs = semua_jadwal(c)
    terpakai, ada = album_terpakai(docs)
    urut = {w: sorted(n for n in ALBUM if ALBUM[n][1] == w) for w in ("biru", "putih")}
    antre = {w: [n for n in urut[w] if n not in terpakai[w]] or urut[w] for w in urut}
    print(f"roll reel IG SS | sudah dipakai: biru={sorted(terpakai['biru']) or '-'} "
          f"putih={sorted(terpakai['putih']) or '-'} | horizon {hari} hari")

    rencana = []
    for ofs in range(hari):
        tgl = hari_ini + datetime.timedelta(days=ofs)
        for jam in SLOT:
            at = iso_utc(tgl, jam)
            if datetime.datetime.fromisoformat(at.replace("Z", "+00:00")) <= sekarang:
                continue
            if at in ada:
                print(f"  ⏭️  {tgl} {jam:02d}:00 WIB — sudah ada, SKIP")
                continue
            w = warna_hari(tgl, jam)
            num = antre[w].pop(0) if antre[w] else urut[w][ofs % len(urut[w])]
            rencana.append((at, tgl, jam, w, num))

    if not rencana:
        print("  semua slot dalam horizon sudah terisi")
        return

    gagal = 0
    for at, tgl, jam, w, num in rencana:
        print(f"\n── {tgl} {jam:02d}:00 WIB | BG {w.upper()} | ss_opt2_{num} ({ALBUM[num][0]}) ──")
        cap = caption(num)
        masalah = valid(cap, cek_link=cek_link)
        if masalah:
            for m in masalah:
                print("  ❗", m)
            gagal += 1
            continue
        if not go:
            print(f"  (dry-run) caption {len(cap)} char | {cap.splitlines()[0][:70]}")
            continue
        out = rakit(num, w, force=force_video)
        if not out:
            gagal += 1
            continue
        print(f"    ✅ {os.path.basename(out)} ({os.path.getsize(out) / 1e6:.2f} MB)")
        u = video_url(num, w)
        if not cek_url_ingress(u):
            print("    ❗ media tidak 200 di kedua ingress — lewati slot ini")
            gagal += 1
            continue
        media = [{"url": u, "type": "video", "alt": os.path.basename(video_path(num, w))}]
        ok = pakai(c, num, w, at, cap, media, go)
        gagal += 0 if ok else 1

    print(f"\nselesai: {len(rencana) - gagal} slot dibuat, {gagal} janggal")


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--hari", type=int, default=0, help="roll horizon N hari (11:00 & 19:00)")
    ap.add_argument("--seri", default=None, help="nomor album (05..20) untuk satu slot manual")
    ap.add_argument("--tgl", default=None, help="YYYY-MM-DD")
    ap.add_argument("--jam", type=int, default=11)
    ap.add_argument("--force-video", action="store_true")
    ap.add_argument("--skip-link-check", action="store_true")
    ap.add_argument("--go", action="store_true")
    a = ap.parse_args()

    if a.hari:
        roll(get_client(), a.hari, a.go, force_video=a.force_video,
             cek_link=not a.skip_link_check)
        return

    if not a.seri:
        raise SystemExit("pakai --hari N (roll) atau --seri NN [--tgl] [--jam]")
    num = a.seri.zfill(2) if len(a.seri) == 2 else a.seri.replace("ss_opt2_", "").zfill(2)
    if num not in ALBUM:
        raise SystemExit(f"album {num} tidak ada; pilihan: {sorted(ALBUM)}")
    tgl = datetime.date.fromisoformat(a.tgl) if a.tgl else datetime.datetime.now(WIB).date()
    w = warna_hari(tgl, a.jam) if a.jam in SLOT else ALBUM[num][1]
    at = iso_utc(tgl, a.jam)
    print(f"reel IG SS | {tgl} {a.jam:02d}:00 WIB | BG {w.upper()} | ss_opt2_{num} ({ALBUM[num][0]})")
    cap = caption(num)
    print(f"\n── caption ({len(cap)} char) ──\n{cap}\n")
    masalah = valid(cap, cek_link=not a.skip_link_check)
    for m in masalah:
        print("❗", m)
    if masalah:
        print("\nBATAL: benahi dulu.")
        return
    if not a.go:
        print("(dry-run — pakai --go)")
        return
    c = get_client()
    docs = semua_jadwal(c)
    ada = {d.get("scheduleAt") for d in docs
           if d.get("accountId") == IG_SS and str(d.get("type")).lower() != "story"}
    if at in ada:
        print(f"\n⏭️  slot {at} sudah terisi — SKIP")
        return
    out = rakit(num, w, force=a.force_video)
    if not out:
        return
    u = video_url(num, w)
    if not cek_url_ingress(u):
        raise SystemExit("media tidak 200 — STOP")
    media = [{"url": u, "type": "video", "alt": os.path.basename(video_path(num, w))}]
    pakai(c, num, w, at, cap, media, True)


if __name__ == "__main__":
    main()
