#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""tsc_story_harian.py — STORY harian Instagram TSC (@theskincode_clinic).

Perintah Bos 16 Okt 2026: *"untuk TSC Instagram pastikan ada story yang tayang tiap hari."*

Cara kerja (pola sama dengan `tslaw_story_harian.py` yang sudah terbukti):
  • Sumber = 22 set carousel 3-slide TSC yang SUDAH pernah tayang di feed (`tsc_car3_*`,
    `tsc_carousel_new`, `tsc_flek/keringat/tahilalat/baukaki` di `openclaw-img/`).
    TIDAK ada desain baru — cuma slide yang sudah ada, dirakit jadi video 9:16.
    Satu story = satu set utuh (slide 1-2-3 berurutan), karena tiap slide bernomor "1/3…3/3".
  • Video: 3 slide x 4 dtk = 12 dtk, 1080x1920 h264+aac (batas story IG 3-60 dtk),
    latar blur + slide utuh di tengah, fade. 1 video per set, dipakai bergilir.
  • Jadwal: `type=story`, caption KOSONG, `replies=[]`, 1 story/hari jam 12:00 WIB.
  • Rotasi deterministik dari tanggal (kelipatan 7 dari 22 set -> tidak ada set kembar
    berurutan), jadi idempoten dan tidak butuh state untuk memilih.
  • Horizon bergulir: pastikan 7 hari ke depan selalu terisi; hari yang sudah punya
    story di-SKIP (anti dobel).

Pakai:
  python3 tsc_story_harian.py preview 7        # rencana (tanpa efek)
  python3 tsc_story_harian.py build 7          # rakit video saja
  python3 tsc_story_harian.py run 7            # dry-run jadwal
  python3 tsc_story_harian.py run 7 --go       # rakit video + buat jadwal + verifikasi
  python3 tsc_story_harian.py verif 14         # baca balik jadwal story yang ada
"""
import argparse
import datetime
import json
import os
import subprocess
import sys
import time

WS = "/home/mantapman/.hermes/profiles/atun/workspace"
IMG = os.path.join(WS, "openclaw-img")
STATE = os.path.join(WS, "tsc_story_state.json")
DUMP = "/tmp/tsc_sched_all.json"
sys.path.insert(0, WS)
from repliz_client import get_client, IMG_BASE_URL  # noqa: E402

TSC_IG = "6a86a7e062caae1e04ff8244"
WIB = datetime.timezone(datetime.timedelta(hours=7))
UTC = datetime.timezone.utc
BASE = IMG_BASE_URL.rstrip("/") + "/openclaw-img/"
JAM = 12                       # 12:00 WIB tiap hari (di luar jam feed 17:00 & thread 10:06)
JAM_HARI_PERTAMA = 19          # 16 Okt: 12:00 sudah lewat -> story pertama jam 19:00 WIB
EPOCH = datetime.date(2026, 10, 16)
DUR = 4.0                      # detik per slide
FADE = 0.4
Y_OFFSET = -70                 # geser slide naik sedikit: hindari area UI bawah IG story
NAMA_SLOT = "TSC IG Story"

# Urut dari yang paling baru tayang di feed.
SETS = [
    "tsc_car3_p31", "tsc_car3_p30", "tsc_car3_p29", "tsc_car3_p28", "tsc_car3_p27",
    "tsc_car3_p26", "tsc_car3_p25", "tsc_car3_p24", "tsc_car3_p23", "tsc_car3_p22",
    "tsc_car3_p21", "tsc_car3_p20", "tsc_car3_p19", "tsc_car3_p18", "tsc_carousel_new",
    "tsc_car3_bjc", "tsc_car3_jrh", "tsc_car3_ksn", "tsc_flek", "tsc_keringat",
    "tsc_tahilalat", "tsc_baukaki",
]
LANGKAH_ROTASI = 7             # 7 dan 22 saling prima -> urutan set tidak kembar berurutan


# ── util ─────────────────────────────────────────────────────────────────────

def slide_files(seri):
    out = []
    for n in (1, 2, 3):
        p = os.path.join(IMG, f"{seri}_{n:02d}.jpg")
        if not os.path.exists(p):
            return []
        out.append(p)
    return out


def video_path(seri):
    return os.path.join(IMG, f"tsc_storyvid_{seri[4:]}.mp4")


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


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


def tgl_wib_dari_iso(iso):
    if not iso:
        return None
    return datetime.datetime.fromisoformat(iso.replace("Z", "+00:00")).astimezone(WIB).date()


def set_untuk(tgl):
    return SETS[((tgl.toordinal() - EPOCH.toordinal()) * LANGKAH_ROTASI) % len(SETS)]


def jam_untuk(tgl, hari_ini):
    return JAM_HARI_PERTAMA if tgl == hari_ini and datetime.datetime.now(WIB).hour >= JAM else JAM


# ── ffmpeg (filtergraph sama dengan ss_ig_story_3x.build_files) ──────────────

def build_files(files, out, dur=DUR, fade=FADE):
    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]split=2[a{i}][b{i}];"
            f"[a{i}]scale={W}:{H}:force_original_aspect_ratio=increase,"
            f"crop={W}:{H},boxblur=28:2[bg{i}];"
            f"[b{i}]scale={W}:-2[fg{i}];"
            f"[bg{i}][fg{i}]overlay=(W-w)/2:(H-h)/2+({Y_OFFSET}),"
            f"fade=t=in:st=0:d={fade},fade=t=out:st={dur - fade}:d={fade},"
            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"  bikin {os.path.basename(out)} ({n} slide, {dur * n:.1f} dtk)")
    subprocess.run(cmd, check=True)


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}: slide tidak lengkap — SKIP")
        return None
    build_files(files, out)
    return out


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

def semua_jadwal(c, fresh=False):
    if not fresh and os.path.exists(DUMP) and time.time() - os.path.getmtime(DUMP) < 3600:
        return json.load(open(DUMP))
    docs, page = [], 1
    while page <= 100:
        r = None
        for a in range(5):
            try:
                r = c.get_schedules(page=page, limit=100)
                break
            except Exception as e:
                print(f"    ⚠️ halaman {page}: {str(e)[:70]}")
                time.sleep(4 + 3 * a)
        if r is None:
            raise SystemExit("gagal baca jadwal Repliz")
        b = r.get("docs") or []
        docs += b
        if len(b) < 100:
            break
        page += 1
    json.dump(docs, open(DUMP, "w"))
    return docs


def story_ada(docs):
    """set tanggal (WIB) yang sudah punya story di TSC IG."""
    ada = {}
    for d in docs:
        if d.get("accountId") == TSC_IG and (d.get("type") or "") == "story":
            t = tgl_wib_dari_iso(d.get("scheduleAt"))
            if t:
                ada[t] = d.get("_id") or d.get("id")
    return ada


def cek_media(url, ulang=3):
    import urllib.request
    for a in range(ulang):
        try:
            return urllib.request.urlopen(url + f"?cb={int(time.time())}", timeout=30).getcode()
        except Exception as e:
            last = str(e)[:80]
            time.sleep(3)
    print(f"    ⚠️ {url.split('/')[-1]}: {last}")
    return 0


def simpan_state(baris):
    lama = []
    if os.path.exists(STATE):
        try:
            lama = json.load(open(STATE)) or []
        except Exception:
            lama = []
    json.dump(lama + baris, open(STATE, "w"), indent=1)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("aksi", choices=["preview", "build", "run", "verif"])
    ap.add_argument("hari", nargs="?", type=int, default=7)
    ap.add_argument("--go", action="store_true")
    ap.add_argument("--force-video", action="store_true")
    a = ap.parse_args()

    hari_ini = datetime.datetime.now(WIB).date()
    sekarang = datetime.datetime.now(UTC)

    if a.aksi == "verif":
        docs = semua_jadwal(get_client(), fresh=True)
        ada = story_ada(docs)
        depan = [hari_ini + datetime.timedelta(days=i) for i in range(a.hari)]
        kurang = [t for t in depan if t not in ada]
        print(f"TSC IG story: {len(ada)} hari punya story (total riwayat)")
        print(f"  jendela {hari_ini}..{depan[-1]}: {len(depan) - len(kurang)} terisi, {len(kurang)} bolong")
        if kurang:
            print("  bolong: " + ", ".join(str(x) for x in kurang))
        return

    rencana = []
    for i in range(a.hari):
        tgl = hari_ini + datetime.timedelta(days=i)
        jam = jam_untuk(tgl, hari_ini)
        at = iso_utc(tgl, jam)
        if datetime.datetime.fromisoformat(at.replace("Z", "+00:00")) <= sekarang:
            continue
        rencana.append((at, tgl, jam, set_untuk(tgl)))

    print(f"TSC IG story — horizon {a.hari} hari ({hari_ini} s/d {hari_ini + datetime.timedelta(days=a.hari - 1)})")
    print(f"  {len(rencana)} slot, {len(SETS)} set dalam rotasi")
    for at, tgl, jam, seri in rencana:
        ada_vid = os.path.exists(video_path(seri))
        print(f"  {tgl} {jam:02d}:00 WIB | {seri} | {'video ada' if ada_vid else 'video BELUM'} | {at}")

    if a.aksi == "preview":
        return

    if a.aksi in ("build", "run"):
        print("\n=== rakit video ===")
        for seri in sorted({s for _, _, _, s in rencana}):
            out = rakit(seri, force=a.force_video)
            if out:
                print(f"  ✅ {os.path.basename(out)} ({os.path.getsize(out) / 1e6:.2f} MB)")
        if a.aksi == "build":
            return

    if not a.go:
        print("\n(dry-run — pakai --go untuk rakit + jadwalkan)")
        return

    print("\n=== cek media di funnel ===")
    for seri in sorted({s for _, _, _, s in rencana}):
        u = video_url(seri)
        kode = cek_media(u)
        print(f"  {kode} {os.path.basename(u)}")
        if kode != 200:
            raise SystemExit("media tidak 200 — stop, jangan bikin jadwal")

    c = get_client()
    docs = semua_jadwal(c, fresh=True)
    ada = story_ada(docs)
    dibuat = []
    print("\n=== buat jadwal ===")
    for at, tgl, jam, seri in rencana:
        if tgl in ada:
            print(f"  ⏭️  {tgl} {jam:02d}:00 WIB — sudah ada story (id={ada[tgl]}), SKIP")
            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(TSC_IG, "", at, title=f"{NAMA_SLOT} {tgl}",
                                        topic=NAMA_SLOT, type_="story", medias=media, replies=[])
                sid = res.get("scheduleId") or res.get("_id") or res.get("id")
                break
            except Exception as e:
                print(f"    ⚠️ percobaan {attempt}: {str(e)[:110]}")
                time.sleep(8)
        print(f"  {'✅' if sid else '❌'} {tgl} {jam:02d} WIB | {seri} | id={sid}")
        if sid:
            dibuat.append({"akun": "tsc_ig", "tanggal": str(tgl), "jam_wib": jam,
                           "set": seri, "video": os.path.basename(video_path(seri)), "id": sid})

    print("\n=== verifikasi (baca ulang per id) ===")
    gagal = 0
    for b in dibuat:
        d = c.get_schedule(b["id"])
        if isinstance(d, dict) and "docs" in d:
            d = d["docs"][0]
        d = d.get("data") or d
        ok = (d.get("status") == "pending" and d.get("type") == "story"
              and not (d.get("replies") or []) and (d.get("medias") or []))
        gagal += 0 if ok else 1
        print(f"  {'✅' if ok else '⚠️'} {b['tanggal']} {b['jam_wib']:02d}:00 | status={d.get('status')} "
              f"type={d.get('type')} media={(d.get('medias') or [{}])[0].get('url', '').split('/')[-1]}")
    if dibuat:
        simpan_state(dibuat)
    print(f"\nselesai: {len(dibuat)} jadwal story dibuat, {gagal} janggal, state: {STATE}")


if __name__ == "__main__":
    main()
