#!/usr/bin/env python3
"""DAILY IMAGE POSTING — cron 2026-10-07T18:00Z (= 8 Okt 2026 01:00 WIB)
Item index 67: uky_quote_waktu_vs_uang -> uky3_output/waktu_vs_uang_biru.png
(brand ukyhandoko, source uky3_output) -> Threads @ukyhandoko.

Tema kartu (vision_analyze, square 1080x1080, bg #122B63 biru tua + teks putih,
standar 16 Sep): header "@ukyhandoko" + "1/1", judul "Waktu Adalah Mata Uang
Paling Berharga", body "Orang bilang time is money. Tapi sebenarnya time is more
than money. Uang bisa dicari lagi. Waktu yang udah lewat gak akan balik. Makanya
gue selalu pilih: delegasin apa yang bisa didelegasi, otomasi apa yang bisa
diotomasi, fokus di hal yang cuma gue bisa lakuin." footer "@ukyhandoko - Founder
StudioSoft". Layout rata kiri, satu blok tengah vertikal, tanpa separator. OK.

NADA (perintah Bos 5 Okt 2026): caption sosmed santai/ngobrol -> "aku"/"kamu".
Kartu memakai "gue" (naskah lama) -> caption TIDAK meniru "gue" (aturan: jangan
alay), pakai "aku".

Distribusi tone. Window pool (entri log #36..#63 = era hook pool "Anda",
n=28): netral 18 / opini 6 / sanjungan 4 = 64.3/21.4/14.3. Deviasi ke target 60/25/15:
  +netral    -> 65.5/20.7/13.8  dev 11.0
  +opini     -> 62.1/24.1/13.8  dev  4.2   <-- TERKECIL
  +sanjungan -> 62.1/20.7/17.2  dev  8.6
Pilih **opini** (pernyataan berani/pro-kontra ringan tapi sopan).

Aturan seri: Threads <=500 char (target <=480), 0 hashtag, 0 mention internal,
tanpa link (bauran growth: post ber-link 0 komentar), replies=[]. Media PNG -> JPEG
di openclaw-img (prefix uky_quote_), URL publik Tailscale funnel, wajib HTTP 200
sebelum schedule. Seri uky TIDAK pakai WP (info wp_image_map.json saja).
Target akun HANYA Threads @ukyhandoko.

SLOT (aturan job "1 konten/hari/akun" + convention 08:00 WIB):
 - 9 Okt 08:00 WIB TIDAK dipakai: sudah ada image lain 08:03 (17_white_mindset)
   -> jarak 3 menit, kelihatan dobel.
 - hari dianggap TERISI kalau ada jadwal Threads-uky (tipe apa pun) di jendela
   07:30-08:30 WIB. Hari bebas pertama = 10 Okt, berikutnya 11 Okt.

RESCUE (item index 65 uky_quote_social_proof): jadwalnya GAGAL tayang 7 Okt 08:00
WIB karena topic 226 char > 50 (batas topic_tag Threads). Bos: konten gagal
dijadwalkan ulang mulai hari berikutnya, 1 post/hari. Repliz TIDAK bisa
menghidupkan jadwal status=error (PUT hanya mengubah topic/scheduleAt, status
tetap "error") -> jadwal baru dibuat di slot bebas pertama (10 Okt 08:00 WIB),
caption lama dipakai utuh dari log, topic dipendekkan <=45 char, lalu jadwal
error lama DIHAPUS supaya tidak dobel kalau server mencoba retry.
Item index 67 (waktu_vs_uang) lalu masuk slot bebas berikutnya (11 Okt 08:00 WIB).

PITFALL tetap: baris baru caption dibangun dengan chr(10)*2 (NL2), JANGAN escape
backslash-n literal di string. topic Threads WAJIB <=45 char (Repliz tolak >50).
"""
import json, os, re, sys, subprocess
from datetime import datetime, timezone, timedelta
from collections import defaultdict

ROOT = "/home/mantapman/.hermes/profiles/atun/workspace"
sys.path.insert(0, ROOT)
from repliz_client import get_client, ACC, IMG_BASE_URL, IMG_DIR
from PIL import Image
import repliz_openssl_transport

# 21 Sep 2026: TLS curl/urllib ke api.repliz.com diblok (DPI) -> pakai shim openssl.
repliz_openssl_transport.install(verbose=False)

NL2 = chr(10) * 2
UKY_T = ACC.UKY_THREADS

# ── ITEM UTAMA (index 67) ────────────────────────────────────────────────────
ITEM_ID = "uky_quote_waktu_vs_uang"
FILENAME = "waktu_vs_uang_biru.png"
SRC = os.path.join(ROOT, "uky3_output", FILENAME)
MEDIA_NAME = "uky_quote_waktu_vs_uang.jpg"
TITLE = "Waktu Lebih Mahal dari Uang"
TOPIC = "Waktu lebih mahal dari uang"          # 26 char (<=45)

BODY = (
    "Orang bilang time is money. Aku nggak setuju \u2014 waktu lebih mahal dari "
    "uang. Uang bisa dicari lagi, waktu yang udah lewat nggak akan balik."
    + NL2 +
    "Makanya aku pilih: delegasi yang bisa didelegasi, otomasi yang bisa "
    "diotomasi, dan fokus ke yang cuma aku bisa lakuin sendiri."
)

# Pancingan OPINI (pola pool diskusi_engine, versi santai 5 Okt 2026).
HOOK = ("Pendapat aku mungkin agak nyaring: banyak orang ngaku sibuk banget, "
        "padahal yang dikerjain hal yang harusnya bisa didelegasi. Kamu setuju "
        "atau nggak? Yuk diskusi di komentar.")

# ── RESCUE (item index 65, gagal 7 Okt 08:00 WIB) ────────────────────────────
RESCUE_ITEM_ID = "uky_quote_social_proof"
RESCUE_MEDIA = "uky_quote_social_proof.jpg"
RESCUE_TOPIC = "Testimoni klien lebih ampuh dari iklan"   # 38 char (<=45)

CTA_RE = re.compile(r"komentar|pendapat|diskusi|setuju|share|pengalaman|menurut|sepakat|\?", re.I)
CJK_RE = re.compile(
    "[" + chr(0x4E00) + "-" + chr(0x9FFF) + chr(0x3040) + "-" + chr(0x30FF)
    + chr(0xAC00) + "-" + chr(0xD7AF) + "]"
)


def build_caption():
    assert BODY.count(NL2) == 1, "BODY harus punya tepat 1 pemisah paragraf"
    cap = BODY + NL2 + HOOK
    log = json.load(open(os.path.join(ROOT, "image_posting_log.json")))
    for e in log:
        if HOOK in (e.get("caption_final") or ""):
            raise RuntimeError("pancingan sudah dipakai di log - tulis yang lain")
    return cap


_UKY_CACHE = []


def fetch_uky(client, refresh=False):
    if _UKY_CACHE and not refresh:
        return _UKY_CACHE[0]
    r = client.get_schedules(page=1, limit=100)
    pages = (r["totalDocs"] + 99) // 100
    docs = list(r["docs"])
    for p in range(2, pages + 1):
        docs.extend(client.get_schedules(page=p, limit=100)["docs"])
    out = []
    for d in docs:
        acc = d.get("account") or {}
        if (d.get("accountId") or acc.get("id") or acc.get("_id")) != UKY_T:
            continue
        out.append(d)
    if refresh:
        _UKY_CACHE[:] = [out]
    else:
        _UKY_CACHE.append(out)
    return out


def free_days(client, n=4):
    """Hari (WIB) >= besok yang jendela 07:30-08:30 WIB-nya kosong utk Threads uky."""
    besok = (datetime.now(timezone.utc) + timedelta(hours=7)).date() + timedelta(days=1)
    terisi = defaultdict(list)
    for d in fetch_uky(client):
        at = d.get("scheduleAt")
        if not at:
            continue
        dt = datetime.fromisoformat(at.replace("Z", "+00:00")) + timedelta(hours=7)
        terisi[dt.date()].append(dt.strftime("%H:%M"))
    out = []
    hari = besok
    for _ in range(120):
        jam = terisi.get(hari, [])
        if not [h for h in jam if "07:30" <= h <= "08:30"]:
            out.append(hari)
            if len(out) >= n:
                break
        hari += timedelta(days=1)
    return out


def upload():
    """PNG -> JPEG di openclaw-img, return URL publik + path lokal."""
    os.makedirs(IMG_DIR, exist_ok=True)
    img = Image.open(SRC)
    if img.mode in ("RGBA", "LA", "P"):
        img = img.convert("RGB")
    out = os.path.join(IMG_DIR, MEDIA_NAME)
    img.save(out, format="JPEG", quality=90)
    url = f"{IMG_BASE_URL.rstrip('/')}/openclaw-img/{os.path.basename(out)}"
    return url, out


def check_public(url):
    """Simulasi jalur INTERNET (resolve ke ingress Tailscale) - bukan tailnet-direct."""
    codes = {}
    for ip in ("103.84.155.153", "103.84.155.217"):
        r = subprocess.run(
            ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "--max-time", "30",
             "--resolve", f"mantapserver.tail3947bb.ts.net:443:{ip}", url],
            capture_output=True, text=True)
        codes[ip] = r.stdout.strip()
    return codes


def wp_media_check():
    """Langkah 3 job: pakai wp_image_map.json - seri uky tidak pakai WP (info saja)."""
    m = json.load(open(os.path.join(ROOT, "wp_image_map.json")))
    key = f"uky3_output/{FILENAME}"
    ent = m.get(key)
    if not ent:
        print("INFO: tidak ada di wp_image_map.json - normal untuk seri uky (tanpa WP):", key)
        return None
    print(f"WP MAP: {key} -> id={ent['id']} {ent['url']}")
    return ent


def media_hits(client, media_name, refresh=False):
    """Jadwal Threads-uky yang memakai media (match EXACT basename)."""
    hits = []
    for d in fetch_uky(client, refresh=refresh):
        for m in (d.get("medias") or []):
            base = (m.get("url") or "").split("?")[0].rsplit("/", 1)[-1]
            if base == media_name:
                hits.append(d)
                break
    return hits


def main():
    assert os.path.exists(SRC), f"file gambar tidak ada: {SRC}"
    wp_media_check()

    # ── caption item 67 ─────────────────────────────────────────────────────
    cap = build_caption()
    print(f"CAPTION ({len(cap)} chars):")
    print(cap)
    print("--- repr:", repr(cap[:100]))
    assert len(cap) <= 500, f"caption {len(cap)} char > 500"
    assert len(cap) <= 480, f"caption {len(cap)} char > 480 (hindari elipsis truncate)"
    assert NL2 in cap, "caption tidak punya baris kosong ganda (newline asli)"
    assert "\\n" not in cap, "caption memuat backslash-n LITERAL"
    assert not re.search(r"@\w+", cap), "ada mention!"
    assert not re.search(r"https?://", cap), "ada link (seri ini tanpa link)"
    assert CTA_RE.search(cap), "tidak ada pancingan diskusi"
    assert "#" not in cap, "ada hashtag (aturan: 0 hashtag untuk seri ini)"
    assert not CJK_RE.search(cap), "ada teks CJK!"
    assert len(TOPIC) <= 45, f"topic {len(TOPIC)} char (>45)"
    assert len(RESCUE_TOPIC) <= 45, f"topic rescue {len(RESCUE_TOPIC)} char (>45)"

    url, out = upload()
    print("LOCAL JPEG:", out, os.path.getsize(out), "bytes")
    print("PUBLIC URL:", url)
    codes = check_public(url)
    print("INTERNET-PATH CODES:", codes)
    if not any(c == "200" for c in codes.values()):
        print("STOP: URL publik tidak 200 - jangan schedule (aturan media server).")
        return 1

    c = get_client()
    log = json.load(open(os.path.join(ROOT, "image_posting_log.json")))
    cari = {e.get("item_id"): e for e in log}
    res_entry = cari.get(RESCUE_ITEM_ID)
    assert res_entry, "entri log social_proof tidak ditemukan"
    rescue_url = res_entry["media_url"]
    rescue_cap = res_entry["caption_final"]
    old_sid = (res_entry.get("repliz_ids") or {}).get("threads_uky")
    print("RESCUE caption chars:", len(rescue_cap), "| old sid:", old_sid)
    assert len(rescue_cap) <= 500
    rc = check_public(rescue_url)
    print("RESCUE media codes:", rc)
    if not any(v == "200" for v in rc.values()):
        print("STOP: media rescue tidak 200.")
        return 1

    # ── slot: hari bebas pertama = rescue (item tua), berikutnya = item 67 ──
    days = free_days(c)
    print("HARI BEBAS (07:30-08:30 WIB kosong):", [d.isoformat() for d in days])
    if len(days) < 2:
        print("STOP: slot bebas < 2"); return 1
    rescue_day, main_day = days[0], days[1]
    rescue_at = f"{rescue_day.isoformat()}T01:00:00.000Z"
    main_at = f"{main_day.isoformat()}T01:00:00.000Z"
    print(f"SLOT RESCUE: {rescue_at} ({rescue_day} 08:00 WIB)")
    print(f"SLOT ITEM67: {main_at} ({main_day} 08:00 WIB)")

    # ── RESCUE: jadwal baru di slot bebas pertama, lalu hapus jadwal error ──
    r_hits = media_hits(c, RESCUE_MEDIA, refresh=True)
    print(f"RESCUE existing schedules ({RESCUE_MEDIA}): ", [
        (d.get("id"), d.get("status"), d.get("scheduleAt")) for d in r_hits])
    fresh = [d for d in r_hits if d.get("status") != "error"]
    if fresh:
        rid = fresh[0].get("id")
        print("SKIP RESCUE: sudah ada jadwal non-error id=", rid)
    else:
        rres = c.create_schedule(
            UKY_T, rescue_cap, rescue_at,
            title=res_entry.get("title_hint") or "Social Proof Itu Bernilai Lebih dari Iklan",
            topic=RESCUE_TOPIC, type_="image",
            medias=[{"url": rescue_url, "type": "image", "alt": "social_proof_biru.png"}],
            replies=[],
        )
        rid = rres.get("scheduleId") or rres.get("_id") or rres.get("id")
        print("RESCUE CREATE:", json.dumps(rres)[:300])
        for d in media_hits(c, RESCUE_MEDIA, refresh=True):
            if d.get("status") == "error" and d.get("id") != rid:
                print("DELETE jadwal error lama:", d.get("id"))
                try:
                    print("  ->", json.dumps(c.delete_schedule(d.get("id")))[:120])
                except Exception as e:
                    print("  !! gagal hapus:", e)
    print("RESCUE SID:", rid)

    # ── ITEM 67 ─────────────────────────────────────────────────────────────
    pre = media_hits(c, MEDIA_NAME, refresh=True)
    if pre:
        sid = pre[0].get("id")
        print(f"SKIP: sudah ada {len(pre)} jadwal utk media ini (id={sid})")
    else:
        res = c.create_schedule(
            UKY_T, cap, main_at,
            title=TITLE, topic=TOPIC,
            type_="image",
            medias=[{"url": url, "type": "image", "alt": FILENAME}],
            replies=[],
        )
        sid = res.get("scheduleId") or res.get("_id") or res.get("id")
        print("CREATE RESPONSE:", json.dumps(res)[:300])
    print("SID:", sid)

    # ── VERIFIKASI (re-list, jangan pakai snapshot lama) ────────────────────
    ok_all = True
    info = {}
    for label, mname, want_id in (("ITEM67", MEDIA_NAME, sid), ("RESCUE", RESCUE_MEDIA, rid)):
        hits = media_hits(c, mname, refresh=True)
        print(f"VERIFIKASI {label}: {len(hits)} jadwal dgn media {mname}")
        good = False
        for d in hits:
            wib = datetime.fromisoformat(d["scheduleAt"].replace("Z", "+00:00")) + timedelta(hours=7)
            desc = d.get("description") or ""
            print(f"  id={d.get('id')} status={d.get('status')} type={d.get('type')} "
                  f"at={wib.strftime('%Y-%m-%d %H:%M')} WIB topic_len={len(d.get('topic') or '')}")
            print(f"  media={d['medias'][0]['url']}")
            print(f"  desc({len(desc)}) replies={len(d.get('replies') or [])} "
                  f"newline_asli={NL2 in desc} literal={chr(92)+'n' in desc}")
            if d.get("id") == want_id and d.get("status") in ("pending", "success") and NL2 in desc:
                good = True
                info[label] = (d.get("id"), wib, d.get("status"))
        if not (good and len(hits) == 1):
            print(f"GAGAL {label}: verifikasi tidak pas (harus 1 jadwal, sid cocok, newline asli)")
            ok_all = False
    if not ok_all:
        return 1
    print("OK TERVERIFIKASI: 2 jadwal (item 67 + rescue), media + caption terpasang")

    # ── UPDATE STATE + LOG ──────────────────────────────────────────────────
    lp = os.path.join(ROOT, "image_posting_log.json")
    log = json.load(open(lp))
    sp = os.path.join(ROOT, "image_posting_state.json")
    st = json.load(open(sp))

    # log rescue (idempoten)
    for e in log:
        if e.get("item_id") == RESCUE_ITEM_ID:
            e.setdefault("rescue", {})
            e["rescue"].update({
                "reason": "jadwal 2026-10-07 08:00 WIB gagal: topic 226 char > 50 (topic_tag Threads)",
                "old_repliz_id": old_sid,
                "new_repliz_id": rid,
                "new_slot": f"{rescue_day.isoformat()} 08:00 WIB",
                "topic_baru": RESCUE_TOPIC,
                "old_error_deleted": True,
            })
            e.setdefault("schedule_times", {})["threads_uky_rescue"] = \
                f"{rescue_at} ({rescue_day} 08:00 WIB) - image 1 (pengganti jadwal error)"
            e.setdefault("repliz_ids", {})["threads_uky_rescue"] = rid

    sudah = any(e.get("item_id") == ITEM_ID for e in log)
    if sudah:
        print("INFO: item sudah tercatat di log - state tidak di-increment lagi")
    else:
        st["index"] = (st["index"] + 1) % st["total"]
        json.dump(st, open(sp, "w"), indent=2)
    print("STATE ->", st)

    if not sudah:
        sid67, wib67, status67 = info["ITEM67"]
        log.append({
            "timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z"),
            "item_id": ITEM_ID, "brand": "ukyhandoko", "source": "uky3_output",
            "filename": FILENAME, "topic": TOPIC,
            "slot_note": f"slot 08:00 WIB bebas (jendela 07:30-08:30 kosong) = {main_day} 08:00 WIB "
                         f"(9 Okt dilewati: sudah ada image 08:03)",
            "schedule_times": {"threads_uky": f"{main_at} ({main_day} 08:00 WIB) - image 1"},
            "repliz_ids": {"threads_uky": sid67},
            "caption_chars": len(cap), "media_url": url,
            "caption_final": cap,
            "next_index": st["index"],
            "hook_note": "opini (custom, pola pool diskusi_engine) - versi santai 5 Okt 2026 "
                         "(pool aslinya 'Anda'), 0 kali dipakai di log; dev minimum 4.2",
        })
    json.dump(log, open(lp, "w"), indent=2, ensure_ascii=False)
    print("LOG ->", lp, f"({len(log)} entri)")
    return 0


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