#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""IG StudioSoft — reel di slot 23:00 WIB dari stok carousel Threads BERLATAR PUTIH.

Perintah Bos 30 Sep 2026 (22:35 WIB): *"Pasang reels untuk IG StudioSoft jam 23.00 cari backroad
putih. Reels video (stok dari carousel threads)"* — jadi reel tambahan di jam 23:00 WIB, videonya
diambil dari stok carousel Threads StudioSoft yang **latar belakangnya putih**, bukan yang navy.

Stok putih yang ada (album carousel Threads SS, `studiosoft_albums_fixed` → `openclaw-img`):
  ss_opt2_06 riset keyword pemula      ss_opt2_14 manfaat digital marketing untuk UMKM
  ss_opt2_08 backlink berkualitas      ss_opt2_16 landing page bisnis kecil
  ss_opt2_10 SEO lokal Surabaya        ss_opt2_18 konten IG yang dilirik
  ss_opt2_20 budget medsos UMKM
(Album 05/07/09/11/13/17/19 latar biru #122B63 → dipakai pipeline navy yang sudah jalan.)

Yang dikerjakan script ini:
  1. Rakit MP4 1080x1920 dari 4 slide album putih → **kanvas PUTIH** (bukan latar blur),
     slide utuh di tengah, fade putih antar slide. TIDAK ada desain baru.
  2. Caption: pembuka + ajakan komentar warisan dari postingan Threads album yang sama
     (sudah tayang), diisi ringkasan isi slide + link artikel studiosoft.id yang hidup + CTA.
  3. Cek media 200 di KEDUA ingress funnel, baru create jadwal.
  4. Idempoten: kalau slot (akun + scheduleAt) sudah ada → SKIP.

Pakai:
  python3 ss_ig_reel_putih.py                          # dry-run, seri sesuai tanggal
  python3 ss_ig_reel_putih.py --seri 10 --jam 23 --go   # rakit + jadwalkan hari ini
"""
import argparse
import datetime
import os
import re
import subprocess
import sys
import time
import urllib.request

WS = "/home/mantapman/.hermes/profiles/atun/workspace"
IMG = f"{WS}/openclaw-img"
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))
JAM = 23
TOPIK = "IG StudioSoft Reel Putih"
CTA = "Bagian mana yang paling relevan buat bisnismu? Cerita di komentar! 👇"

# seri album putih (nomor album -> judul album, sumber ss_album_content.json)
PUTIH = {
    "06": "Riset keyword pemula",
    "08": "Backlink berkualitas",
    "10": "SEO lokal Surabaya",
    "14": "Manfaat digital marketing untuk UMKM",
    "16": "Landing page bisnis kecil",
    "18": "Konten IG yang dilirik",
    "20": "Budget medsos UMKM",
}
LINK_LANGSUNG = {
    "06": "https://studiosoft.id/cara-klinik-kecantikan-di-surabaya-bisa-ditemukan-lebih-cepat-di-google/",
    "08": "https://studiosoft.id/tips-seo-digital-marketing-surabaya-optimasi-local-untuk-klinik-dan-bengkel-lokal/",
    "10": "https://studiosoft.id/tips-seo-digital-marketing-surabaya-optimasi-local-untuk-klinik-dan-bengkel-lokal/",
    "14": "https://studiosoft.id/jasa-digital-marketing-klinik/",
    "16": "https://studiosoft.id/tips-seo-digital-marketing-surabaya-optimasi-local-untuk-klinik-dan-bengkel-lokal/",
    "18": "https://studiosoft.id/jasa-digital-marketing-klinik/",
    "20": "https://studiosoft.id/jasa-digital-marketing-klinik/",
}

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)


# ── video ────────────────────────────────────────────────────────────────

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


def video_path(seri):
    return f"{IMG}/ss_reel_putih_{seri[-2:]}.mp4"


def video_url(seri):
    return BASE + os.path.basename(video_path(seri))


def build(files, out, dur=4.0, fade=0.35):
    """Rakit 1080x1920 dari slide 4:5, kanvas PUTIH (bukan latar blur)."""
    W, H = 1080, 1920
    n = len(files)
    cmd = ["ffmpeg", "-y", "-v", "error"]
    for f in files:
        cmd += ["-loop", "1", "-t", str(dur), "-i", f]
    parts, labels = [], []
    for i in range(n):
        parts.append(
            f"[{i}:v]scale={W}:-2,pad={W}:{H}:0:(oh-ih)/2:color=white,"
            f"fade=t=in:st=0:d={fade}:color=white,"
            f"fade=t=out:st={dur - fade}:d={fade}:color=white,"
            f"setsar=1[v{i}]"
        )
        labels.append(f"[v{i}]")
    fc = ";".join(parts) + ";" + "".join(labels) + f"concat=n={n}:v=1:a=0[vout]"
    cmd += [
        "-f", "lavfi", "-t", str(dur * n), "-i", "anullsrc=r=44100:cl=stereo",
        "-filter_complex", fc, "-map", "[vout]", "-map", f"{n}:a",
        "-c:v", "libx264", "-preset", "medium", "-crf", "22",
        "-pix_fmt", "yuv420p", "-profile:v", "high", "-level", "4.0",
        "-r", "30", "-c:a", "aac", "-b:a", "128k", "-shortest",
        "-movflags", "+faststart", out,
    ]
    print(f"  rakit {os.path.basename(out)} ({n} slide x {dur} dtk = {dur * n:.1f} dtk, kanvas putih)")
    subprocess.run(cmd, check=True)
    return out


def rakit(seri, force=False):
    out = video_path(seri)
    if os.path.exists(out) and os.path.getsize(out) > 100_000 and not force:
        return out
    files = slide_files(seri)
    if not files:
        print(f"  ⚠️ {seri}: file slide tidak ada — STOP")
        return None
    build(files, out)
    return out


# ── caption ──────────────────────────────────────────────────────────────

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 caption(seri):
    num = seri[-2:]
    judul = PUTIH[num]
    link = LINK_LANGSUNG[num]
    cap = (
        f"Rangkuman tips {judul} yang sering kami bagikan ke klien. 📊\n\n"
        "Poin pentingnya: mulai dari yang paling dekat dengan pelanggan, ukur hasilnya, lalu "
        "jaga konsistensi tiap minggu.\n\n"
        f"Kalau dijabarkan untuk bisnis di Surabaya: {JABARAN[num]}\n\n"
        f"Versi lengkapnya ada di artikel kami: {link}\n\n"
        f"{CTA}\n\n"
        "#studiosoft #seolokal #digitalmarketing #bisnissurabaya #umkm"
    )
    return cap


JABARAN = {
    "10": ("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\"."),
    "06": ("riset keyword itu bukan menebak. Lihat apa yang orang ketik, pilih long-tail yang "
           "kompetisinya kecil, sesuaikan dengan niat beli, dan tools gratis sudah cukup untuk mulai."),
    "08": ("backlink itu rekomendasi digital. Satu rujukan dari situs yang dipercaya lebih berguna "
           "daripada sepuluh tautan asal-asalan."),
    "14": ("digital marketing memangkas biaya promosi, membuka pasar lebih luas, dan membuat hasilnya "
           "terukur — tiga hal yang paling dicari pemilik usaha kecil."),
    "16": ("landing page yang baik fokus pada satu tujuan, memuat cepat di HP, dan menuntun pengunjung "
           "ke satu aksi saja."),
    "18": ("konten yang dilihat orang biasanya menjawab satu pertanyaan spesifik, bukan sekadar "
           "menampilkan produk."),
    "20": ("budget media sosial tidak harus besar. Yang penting tahu mana yang mendatangkan "
           "pelanggan, bukan mana yang paling ramai."),
}


def valid(cap):
    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)}")
    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


# ── jadwal ───────────────────────────────────────────────────────────────

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 as e:
                print(f"    ⚠️ halaman {page} gagal ({str(e)[:50]}), ulang…")
                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):
    """200 di kedua ingress (jalur internet), bukan jalur tailnet lokal."""
    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 album_dipakai(docs):
    """Nomor album putih yang sudah pernah jadi reel IG SS + slot yang sudah terisi."""
    terpakai, ada = set(), set()
    urut = []
    for d in docs:
        if d.get("accountId") != IG_SS:
            continue
        if d.get("type") != "story":
            ada.add(d.get("scheduleAt"))
        for m in (d.get("medias") or []):
            mm = re.match(r"ss_reel_putih_(\d\d)\.mp4$", (m.get("url") or "").split("/")[-1])
            if mm:
                terpakai.add(mm.group(1))
                urut.append((d.get("scheduleAt") or "", mm.group(1)))
    return terpakai, ada


def roll(c, hari, go, force_video=False):
    """Jaga horizon `hari` hari ke depan: 1 reel putih/hari di jam JAM (default 23:00 WIB)."""
    hari_ini = datetime.datetime.now(WIB).date()
    sekarang = datetime.datetime.now(datetime.timezone.utc)
    docs = semua_jadwal(c)
    terpakai, ada = album_dipakai(docs)
    urut = sorted(PUTIH)
    antre = [n for n in urut if n not in terpakai] or urut   # habis -> ulang dari awal
    print(f"roll reel putih IG SS | album sudah dipakai: {sorted(terpakai) or '-'} | "
          f"antre: {antre} | horizon {hari} hari")

    rencana = []
    for ofs in range(hari):
        tgl = hari_ini + datetime.timedelta(days=ofs)
        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
        num = antre.pop(0) if antre else urut[ofs % len(urut)]
        rencana.append((at, tgl, f"ss_opt2_{num}"))

    if not rencana:
        print("  semua slot dalam horizon sudah terisi — tidak ada yang dibuat")
        return

    gagal = 0
    for at, tgl, seri in rencana:
        print(f"\n── {tgl} {JAM:02d}:00 WIB | {seri} ({PUTIH[seri[-2:]]}) ──")
        cap = caption(seri)
        masalah = valid(cap)
        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(seri, force=force_video)
        if not out:
            gagal += 1
            continue
        print(f"  ✅ {os.path.basename(out)} ({os.path.getsize(out) / 1e6:.2f} MB)")
        if not cek_url_ingress(video_url(seri)):
            print("  ❗ media tidak 200 di kedua ingress — lewati slot ini")
            gagal += 1
            continue
        media = [{"url": video_url(seri), "type": "video",
                  "alt": os.path.basename(video_path(seri))}]
        sid = None
        for attempt in range(1, 4):
            try:
                res = c.create_schedule(IG_SS, cap, at, title=f"Reel IG SS putih {tgl}",
                                        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:
            gagal += 1
            print("  ❌ create gagal")
            continue
        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"
        gagal += 0 if ok else 1
        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]}")
    print(f"\nselesai: {len(rencana) - gagal} slot dibuat, {gagal} janggal")


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--seri", default=None, help="nomor album putih (06/08/10/14/16/18/20)")
    ap.add_argument("--tgl", default=None, help="YYYY-MM-DD (default: hari ini WIB)")
    ap.add_argument("--jam", type=int, default=JAM)
    ap.add_argument("--hari", type=int, default=0, help="kalau >0 = mode roll horizon N hari")
    ap.add_argument("--force-video", 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)
        return

    hari_ini = datetime.datetime.now(WIB).date()
    tgl = datetime.date.fromisoformat(a.tgl) if a.tgl else hari_ini
    pilihan = list(PUTIH)
    seri = f"ss_opt2_{a.seri}" if a.seri else f"ss_opt2_{pilihan[tgl.toordinal() % len(pilihan)]}"
    if seri[-2:] not in PUTIH:
        raise SystemExit(f"album {seri} bukan album putih; pilih {pilihan}")

    at = iso_utc(tgl, a.jam)
    sekarang = datetime.datetime.now(datetime.timezone.utc)
    print(f"reel IG SS 23:00-an | seri {seri} ({PUTIH[seri[-2:]]}) | {tgl} {a.jam:02d}:00 WIB | {at}")

    cap = caption(seri)
    print(f"\n── caption ({len(cap)} char) ──\n{cap}\n")
    masalah = valid(cap)
    for m in masalah:
        print("❗", m)
    if masalah:
        print("\nBATAL: benahi caption dulu.")
        return

    out = rakit(seri, force=a.force_video)
    if not out:
        return
    print(f"  ✅ {os.path.basename(out)} ({os.path.getsize(out) / 1e6:.2f} MB)")

    print("\n── cek media di kedua ingress ──")
    if not cek_url_ingress(video_url(seri)):
        raise SystemExit("media tidak 200 di kedua ingress — STOP, jangan bikin jadwal")

    if datetime.datetime.fromisoformat(at.replace("Z", "+00:00")) <= sekarang:
        print("\nslot sudah lewat — tidak dijadwalkan (pakai --tgl besok)")
        return

    if not a.go:
        print("\n(dry-run — pakai --go untuk create jadwal)")
        return

    c = get_client()
    docs = semua_jadwal(c)
    ada = {d.get("scheduleAt") for d in docs
           if d.get("accountId") == IG_SS and d.get("type") != "story"}
    if at in ada:
        print(f"\n⏭️  slot {at} di IG SS sudah terisi — SKIP (idempoten)")
        return

    media = [{"url": video_url(seri), "type": "video", "alt": os.path.basename(video_path(seri))}]
    sid = None
    for attempt in range(1, 4):
        try:
            res = c.create_schedule(IG_SS, cap, at, title=f"Reel IG SS putih {tgl} {a.jam:02d}",
                                    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)
    print(f"\n{'✅' if sid else '❌'} create {tgl} {a.jam:02d}:00 WIB | {seri} | id={sid}")
    if not sid:
        return

    print("\n=== verifikasi (baca ulang dari API) ===")
    d = c.get_schedule(sid)
    d = d.get("data") or d
    if isinstance(d, dict) and "docs" in d:
        d = d["docs"][0]
    print(f"  status={d.get('status')} type={d.get('type')} at={d.get('scheduleAt')} "
          f"media={(d.get('medias') or [{}])[0].get('url', '').split('/')[-1]} "
          f"isShareToFeed={(d.get('additionalInfo') or {}).get('isShareToFeed')} "
          f"replies={len(d.get('replies') or [])} cap={len(d.get('description') or '')}")


if __name__ == "__main__":
    main()
