#!/usr/bin/env python3
"""DAILY IMAGE POSTING — cron 2026-10-15T18:00Z (= 16 Okt 2026 01:00 WIB run)
Antrean image_posting_queue.json, state masuk index=85 (total 88).

RUN INI = EKOR SERI: #85 & #86 di-SKIP, #87 DIPOSTING -> index wrap ke 0.

AUDIT (protokol references/image-queue-duplikat.md, korpus 290 jadwal Threads-uky
di-dump ulang ke catatan_korpus_uky_20261014.json, skrip _audit_uky42_kandidat84_87.py
+ _audit_uky42_lapis2.py):

 #85 uky_quote4_ai_ikut_bekerja_kamu_memutuskan -> SKIP (duplikat hampir verbatim).
   Kartu: "Saya pakai AI untuk mengumpulkan bahan dan merapikan tulisan. Bagian opini
   dan keputusan tetap milik manusia." Tiga klausa kuncinya sudah tayang:
   2026-10-22 08:48 WIB (text, 'AI & Otomasi'): "AI buat mengumpulkan bahan mentah:
   riset, draft, rangkuman panjang ... Bagian opini, pengalaman, dan keputusan tetap
   saya tulis sendiri";
   2026-10-04 14:00 WIB (text, 'Daily Threads'): "AI saya pakai untuk cari ide dan
   merapikan tulisan. Yang tidak saya serahkan: pengalaman dan opini";
   2026-11-03 08:00 WIB kartu 'AI alat, bukan pengganti otak' = item #70 antrean INI
   SENDIRI yang sudah tayang.

 #86 uky_quote4_konsisten_mengalahkan_viral -> SKIP (duplikat hampir verbatim).
   Kartu: "Viral itu bonus. Yang menghidupi bisnis adalah orang yang ingat kamu saat
   mereka butuh, bukan yang cuma lewat sekali."
   vs 2026-10-08 20:17 WIB (text, 'Konten & Media Sosial'): "Viral itu bonus, bukan
   target. Yang bikin bisnis jalan itu orang yang ingat kamu saat butuh, bukan orang
   yang pernah ketawa sekali lihat kontenmu." Frasa kunci identik; tema
   konsistensi-vs-viral juga sudah jadi topik/baris-1 3x (30 Sep 05:00, 8 Okt 20:17,
   31 Okt 09:20).

 #87 uky_quote4_data_tanpa_konteks -> DIPOSTING. Frasa kunci kartu ("tanpa konteks",
   "tahu pertanyaan", "laporan setebal", "angka baru", "jadi bacaan") 0 kemunculan di
   290 jadwal. Tetangga beda tesis: 20 Okt 12:22 'Fotografi & Visual' ("berapa angka
   yang benar-benar dibaca klien sampai habis" = laporan TIDAK DIBACA),
   28 Okt 12:23 'Data & Pengukuran' (ukur mingguan tanpa tunggu laporan bulanan),
   18 Nov 03:06 (laporan iklan 5 juta tapi kata kunci tidak pernah dicek),
   16 Sep 08:00 'Metrik vs dampak' (angka bagus tanpa dampak). Risiko tumpang tindih
   RINGAN -> lanjut, dilaporkan ke Bos.

KARTU (standar 16 Sep 2026): square 1080x1080, varian ORANGE seri uky4 (bg (249,115,22)
#F97316 + teks putih; rotasi varian seri: #83 putih -> #84 orange -> #87 orange karena
#85 (biru) & #86 (putih) di-skip — sesuai aturan "queue yang menentukan varian, jangan
swap aset"), header "@ukyhandoko" + "1/1", footer "@ukyhandoko • Founder StudioSoft",
rata kiri, satu blok tengah, tanpa separator, 0 CJK (diverifikasi vision_analyze).

NADA (perintah Bos 5 Okt 2026): santai ("aku"/"kamu"). Distribusi tone pool (hook
in caption_final, n=25): netral 15 / opini 6 / sanjungan 4.
  +netral    -> 16/6/4 = 61.5/23.1/15.4  dev 3.85  <-- TERPILIH
  +opini     -> 15/7/4 = 57.7/26.9/15.4  dev 4.62
  +sanjungan -> 15/6/5 = 57.7/23.1/19.2  dev 8.46
Lane kartu 08:00 dua entri terakhir = opini (#83), opini (#84) -> netral menyeimbangkan.

Aturan lane kartu 08:00 uky: Threads <=500 char (target <=480), 0 hashtag, 0 mention
internal, tanpa link, replies=[]. topic (topic_tag) WAJIB <=45 char.

SLOT: lane kartu uky4 item #84 = 8 Nov 2026 08:00 WIB -> lanjut 9 Nov 2026 08:00 WIB
(dicek API run ini: jendela 07:30-08:30 WIB 9 Nov kosong untuk Threads-uky).

Upload WP: DILEWATI — wp_image_map.json tidak punya entri `uky4_output/*` (seri uky
memakai media server openclaw-img, bukan WP; upload WP di prompt job = skip).

PITFALL: baris baru caption dibangun dengan chr(10)*2, JANGAN backslash-n literal.
"""
import json, os, re, sys, subprocess
from datetime import datetime, timezone, timedelta

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

repliz_openssl_transport.install(verbose=False)

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

ITEM_ID = "uky_quote4_data_tanpa_konteks"
FILENAME = "data_tanpa_konteks_orange.png"
SRC = os.path.join(ROOT, "uky4_output", FILENAME)
MEDIA_NAME = "uky_quote_data_tanpa_konteks.jpg"
TITLE = "uky Data Tanpa Konteks Cuma Angka 2026-11-09"
TOPIC = "Angka baru berguna kalau tahu pertanyaannya"      # 43 char (<=45)

SLOT_AT = "2026-11-09T01:00:00.000Z"                       # 9 Nov 2026 08:00 WIB
SLOT_WIB = "2026-11-09 08:00 WIB"
SLOT_DATE = datetime(2026, 11, 9).date()

BODY = (
    "Angka itu bukan jawaban. Angka cuma bahan. Kalau pertanyaannya belum jelas, "
    "laporan setebal apa pun cuma jadi bacaan."
    + NL2 +
    "Aku belajar ini pelan-pelan: sebelum minta data, tulis dulu pertanyaan yang "
    "mau dijawab. Setelah itu, satu halaman biasanya sudah cukup buat ngambil keputusan."
)

HOOK = "Kalau kamu, pertanyaan apa yang paling sering kamu tanya sebelum lihat laporan? Cerita di komentar ya \U0001F447"

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) + "]"
)

# frasa kunci kartu #87 — tidak boleh pernah jadi topic/baris pertama post lain
FRASA87 = re.compile(
    r"tanpa konteks|tahu pertanyaan|laporan setebal|angka baru|jadi bacaan|"
    r"pertanyaan yang mau dijawab", re.I)


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")
        if e.get("item_id") == ITEM_ID:
            raise RuntimeError("item sudah pernah dicatat di log - jangan dobel")
    return cap


_UKY = []


def fetch_uky(client, refresh=False):
    if _UKY and not refresh:
        return _UKY[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 = [d for d in docs
           if (d.get("accountId") or (d.get("account") or {}).get("id")) == UKY_T]
    if refresh:
        _UKY[:] = [out]
    else:
        _UKY.append(out)
    return out


def wib(d):
    at = d.get("scheduleAt")
    return None if not at else datetime.fromisoformat(at.replace("Z", "+00:00")) + timedelta(hours=7)


def media_hits(client, media_name, refresh=False):
    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 upload():
    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):
    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 main():
    lp = os.path.join(ROOT, "image_posting_log.json")
    sp = os.path.join(ROOT, "image_posting_state.json")
    log = json.load(open(lp))
    st = json.load(open(sp))
    print("STATE MASUK:", st)
    assert st["index"] == 85, f"index state {st['index']} != 85 — antrean berubah, stop"

    # ── 1. ASET ───────────────────────────────────────────────────────────────
    assert os.path.exists(SRC), f"file gambar tidak ada: {SRC}"
    im = Image.open(SRC).convert("RGB")
    assert im.size == (1080, 1080), f"ukuran {im.size} != 1080x1080"
    bg = im.getpixel((5, 540))
    print(f"ASET: {SRC} size={im.size} bg_px={bg}")
    assert bg == (249, 115, 22), f"bg {bg} bukan varian ORANGE seri uky4"
    print("VARIAN: ORANGE (bg #F97316 + teks putih) — #85 biru & #86 putih di-skip, "
          "queue yang menentukan varian (jangan swap aset)")

    # ── 2. SKIP #85 & #86 (dicatat ke log di langkah 8, setelah sukses) ────────
    now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z")
    skip85 = {
        "timestamp": now,
        "item_id": "uky_quote4_ai_ikut_bekerja_kamu_memutuskan",
        "brand": "ukyhandoko", "source": "uky4_output", "status": "SKIPPED",
        "filename": "ai_ikut_bekerja_kamu_memutuskan_biru.png (kartu square 1080x1080, "
                    "varian BIRU, seri uky4)",
        "reason": "DUPLIKAT hampir verbatim: kartu #85 \"Saya pakai AI untuk mengumpulkan "
                  "bahan dan merapikan tulisan. Bagian opini dan keputusan tetap milik "
                  "manusia.\" Ketiga klausa kuncinya sudah tayang: 2026-10-22 08:48 WIB "
                  "(text, topic 'AI & Otomasi'): \"AI buat mengumpulkan bahan mentah: "
                  "riset, draft, rangkuman panjang … Bagian opini, pengalaman, dan "
                  "keputusan tetap saya tulis sendiri\"; 2026-10-04 14:00 WIB (text, "
                  "'Daily Threads'): \"AI saya pakai untuk cari ide dan merapikan tulisan. "
                  "Yang tidak saya serahkan: pengalaman dan opini\"; 2026-11-03 08:00 WIB "
                  "kartu 'AI alat, bukan pengganti otak' = item #70 antrean INI SENDIRI "
                  "yang sudah tayang. Tidak ada jadwal dibuat.",
        "action": "skip, lanjut item berikutnya",
    }
    skip86 = {
        "timestamp": now,
        "item_id": "uky_quote4_konsisten_mengalahkan_viral",
        "brand": "ukyhandoko", "source": "uky4_output", "status": "SKIPPED",
        "filename": "konsisten_mengalahkan_viral_putih.png (kartu square 1080x1080, "
                    "varian PUTIH, seri uky4)",
        "reason": "DUPLIKAT hampir verbatim: kartu #86 \"Viral itu bonus. Yang menghidupi "
                  "bisnis adalah orang yang ingat kamu saat mereka butuh, bukan yang cuma "
                  "lewat sekali.\" vs 2026-10-08 20:17 WIB (text, topic 'Konten & Media "
                  "Sosial'): \"Viral itu bonus, bukan target. Yang bikin bisnis jalan itu "
                  "orang yang ingat kamu saat butuh, bukan orang yang pernah ketawa sekali "
                  "lihat kontenmu.\" Frasa kunci identik; tema konsistensi-vs-viral juga "
                  "sudah jadi topik/baris-1 3x (30 Sep 05:00 'branding', 8 Okt 20:17, "
                  "31 Okt 09:20 'Konsistensi brand: repetisi = kepercayaan'). Tidak ada "
                  "jadwal dibuat.",
        "action": "skip, lanjut item berikutnya",
    }
    log.append(skip85)
    log.append(skip86)
    print("SKIP #85 & #86 akan dicatat di log (langkah 8).")

    # ── 3. CAPTION ────────────────────────────────────────────────────────────
    cap = build_caption()
    print(f"CAPTION ({len(cap)} chars):")
    print(cap)
    print("REPR:", repr(cap[:70]))
    assert len(cap) <= 500, f"caption {len(cap)} char > 500"
    assert len(cap) <= 480, f"caption {len(cap)} char > 480"
    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 (lane 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 "gue" not in cap.lower(), "ada kata 'gue'"
    assert len(TOPIC) <= 45, f"topic {len(TOPIC)} char (>45)"
    cek_teks_cjk(cap, TITLE, TOPIC, konteks="image_post uky #87")

    c = get_client()

    # ── 4. AUDIT TEMA #87 ────────────────────────────────────────────────────
    semua = fetch_uky(c, refresh=True)
    kemunculan, jadi_topik = [], []
    for d in semua:
        desc = d.get("description") or ""
        if not FRASA87.search(desc):
            continue
        kemunculan.append(d)
        baris1 = desc.strip().splitlines()[0] if desc.strip() else ""
        if FRASA87.search(baris1) or FRASA87.search(d.get("topic") or ""):
            jadi_topik.append(d)
    print(f"CEK TEMA frasa {FRASA87.pattern!r} di {len(semua)} jadwal Threads-uky: "
          f"{len(kemunculan)} kemunculan, {len(jadi_topik)} di antaranya jadi topic/baris pertama")
    for d in kemunculan:
        print("   ", wib(d).strftime("%Y-%m-%d %H:%M"), d.get("status"), d.get("type"),
              "| topic:", repr((d.get("topic") or "")[:34]), "|",
              (d.get("description") or "").strip().splitlines()[0][:70])
    assert not jadi_topik, "frasa kunci sudah pernah jadi topik/judul post lain - jangan posting"
    print("   -> 0 kemunculan sebagai topic/baris pertama = tema kartu #87 belum pernah tayang.")

    # ── 5. SLOT ───────────────────────────────────────────────────────────────
    same_day = [d for d in fetch_uky(c) if wib(d) and wib(d).date() == SLOT_DATE
                and "07:30" <= wib(d).strftime("%H:%M") <= "08:30"]
    all_day = [d for d in fetch_uky(c) if wib(d) and wib(d).date() == SLOT_DATE]
    print(f"Jadwal Threads-uky {SLOT_DATE}: total {len(all_day)} "
          f"{[(wib(d).strftime('%H:%M'), d.get('type')) for d in all_day]} | "
          f"jendela 07:30-08:30: {[(wib(d).strftime('%H:%M'), d.get('type')) for d in same_day]}")
    assert not same_day, f"slot {SLOT_DATE} tidak kosong - pilih hari lain"
    assert not media_hits(c, MEDIA_NAME, refresh=True), "media ini sudah punya jadwal"

    # ── 6. UPLOAD + CHECK URL ─────────────────────────────────────────────────
    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(v == "200" for v in codes.values()):
        print("STOP: URL publik tidak 200 - jangan schedule (aturan media server).")
        return 1

    # ── 7. CREATE + VERIFIKASI ────────────────────────────────────────────────
    res = c.create_schedule(
        UKY_T, cap, SLOT_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], "-> SID:", sid)

    hits = media_hits(c, MEDIA_NAME, refresh=True)
    print(f"VERIFIKASI: {len(hits)} jadwal dgn media {MEDIA_NAME}")
    ok = False
    for d in hits:
        w = wib(d)
        desc = d.get("description") or ""
        print(f"  id={d.get('id')} status={d.get('status')} type={d.get('type')} "
              f"at={w.strftime('%Y-%m-%d %H:%M')} WIB topic={len(d.get('topic') or '')}c")
        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") == sid and d.get("status") in ("pending", "success")
                and NL2 in desc and (d.get("topic") or "") == TOPIC):
            ok = True
    if not (ok and len(hits) == 1):
        print("GAGAL verifikasi: harus 1 jadwal, sid cocok, topic utuh, newline asli")
        return 1
    print("OK TERVERIFIKASI: 1 jadwal, media + caption + topic terpasang")

    # ── 8. UPDATE STATE + LOG (#85 SKIP, #86 SKIP, #87 POSTED) ────────────────
    st["index"] = (st["index"] + 3) % st["total"]   # 85 -> 88 % 88 = 0 (wrap)
    log.append({
        "timestamp": now,
        "item_id": ITEM_ID, "brand": "ukyhandoko", "source": "uky4_output",
        "filename": f"{FILENAME} (kartu square 1080x1080, varian ORANGE bg #F97316 + "
                    f"teks putih, seri uky4, standar 16 Sep - sesuai, cek vision_analyze)",
        "topic": TOPIC,
        "slot_note": f"lane kartu uky4 lanjutan setelah item #84 (8 Nov 08:00); {SLOT_WIB} "
                     f"- jendela 07:30-08:30 WIB 9 Nov kosong utk Threads-uky (dicek API "
                     f"run ini). Frasa kunci kartu 0 kemunculan di 290 jadwal korpus. "
                     f"#85 & #86 resmi SKIP (duplikat hampir verbatim) -> seri kartu uky4 "
                     f"18 kartu HABIS di #87; index wrap ke 0 (antrean lama Jul-Okt). "
                     f"Bos perlu putuskan: render seri kartu baru atau hentikan loop.",
        "schedule_times": {"threads_uky": f"{SLOT_AT} ({SLOT_WIB}) - image 1"},
        "repliz_ids": {"threads_uky": sid},
        "caption_chars": len(cap), "media_url": url,
        "caption_final": cap, "replies": [], "link": "",
        "verifikasi": f"GET/re-list /public/schedule -> id {sid}, accountId {UKY_T} "
                      f"(Threads @ukyhandoko), type image, scheduleAt {SLOT_AT}, topic "
                      f"{len(TOPIC)} char, media {url} HTTP 200 ({codes}), status pending, replies 0",
        "hook_note": "netral (pertanyaan terbuka + ajak cerita di komentar, nada santai "
                     "5 Okt 2026) - dev minimum 3.85 dari pool n=25 (netral 15/opini 6/"
                     "sanjungan 4); lane kartu 2 entri terakhir opini (#83, #84) -> netral",
        "next_index": st["index"],
        "seri_note": "INI KARTU TERAKHIR seri uky4 (18 kartu, #70-#87). Index antrean "
                     "wrap ke 0 = item #0 ss_batch_20260701 (konten Juli) akan tayang "
                     "ulang kecuali Bos menyiapkan seri/antrean baru.",
    })
    json.dump(log, open(lp, "w"), indent=2, ensure_ascii=False)
    json.dump(st, open(sp, "w"), indent=2)
    print("STATE ->", st)
    print("LOG ->", lp, f"({len(log)} entri)")
    return 0


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