#!/usr/bin/env python3
"""DAILY IMAGE POSTING — cron 2026-10-13T18:00Z (= 14 Okt 2026 01:00 WIB run)
Antrean image_posting_queue.json, state index=78.

AUDIT DUPLIKAT (protocol references/image-queue-duplikat.md) atas SEMUA item 78-83
(uky4_output, Threads @ukyhandoko, korpus 300 jadwal akun ini di-dump ulang):

 #78 uky_quote4_otomatiskan_yang_membosankan -> SKIP. Kartu: "Bukan alat mahal yang menghemat
      waktu saya, tapi menyerahkan pekerjaan yang bentuknya selalu sama ke mesin." Threads
      @ukyhandoko 2026-10-29 20:00 WIB (text, topic 'AI & Otomasi'): "Pengalaman saya dengan
      otomatisasi: yang paling menghemat waktu bukan alat termahal, tapi yang menggantikan
      pekerjaan paling membosankan. Bagi saya itu bukan analisis data, tapi mengumpulkan laporan
      yang bentuknya selalu sama. Setelah itu diserahkan ke mesin, saya punya waktu untuk hal yang
      benar-benar berpikir." = kalimat kunci kartu HAMPIR VERBATIM (alat mahal/termahal, menghemat
      waktu, pekerjaan membosankan, bentuknya selalu sama, diserahkan ke mesin) di akun yang sama.
 #79 uky_quote4_tim_tahu_batasnya -> SKIP. Kartu: "Orang tidak butuh dimotivasi terus. Mereka butuh
      tahu apa tugasnya, sampai kapan, dan apa artinya selesai." 2026-10-31 11:00 WIB (text, topic
      'Tim & Karier'): "orang tidak butuh dimotivasi terus, mereka butuh tahu batasnya. Ketika
      tugasnya jelas dan kapan selesainya jelas, sebagian besar orang bekerja dengan baik tanpa
      perlu ditunggui." = frasa pembuka kartu IDENTIK + isi sama.
 #80 uky_quote4_tidak_itu_strategi -> SKIP (tema). Kartu: "Setiap pekerjaan yang saya ambil karena
      tidak enak menolak, hampir selalu jadi pekerjaan yang saya sesali." Tema 'menolak' sudah
      jadi topik/baris pertama 3x dalam <1 bulan: 2026-10-15 09:35 "Saya pernah menolak klien karena
      briefnya tidak masuk akal secara bisnis", 2026-10-17 09:28 "Saya pernah menolak proyek besar",
      2026-10-17 09:29 "Kalian menolak pekerjaan, atau menambah orang dulu?"; didukung 2026-10-31
      12:51 "saya lebih suka menolak satu pekerjaan tambahan" dan 2026-10-20 20:42 "berani bilang
      tidak ke yang lain". Kartu ke-6 bertema sama dalam 3 minggu = konten berulang.
 #81 uky_quote4_ukur_yang_penting -> SKIP (tema). Kartu: "Jumlah pengikut itu mudah dihitung. Yang
      lebih penting: berapa orang bertanya, berapa yang kembali, berapa yang bercerita ke orang
      lain." Tema 'metrik ramai vs dampak' sudah tayang: 2026-09-16 08:00 (image, topic 'Metrik vs
      dampak') "Dulu saya juga ngejar angka: views, likes, followers ... yang paling berarti justru
      yang nggak kelihatan di dashboard"; 2026-09-15 23:00 "Follower itu bukan target utama";
      2026-10-25 09:00 (album) "Pengikut banyak tidak otomatis jadi pelanggan".
 #82 uky_quote4_website_rumah_bukan_kost -> SKIP. Kartu: "Media sosial tempat menyapa. Website
      tempat orang mengenal kamu seutuhnya, dan tidak bisa diambil orang lain." 2026-10-04 17:00
      WIB (text, topic 'Website'): "media sosial itu tempat menyapa, website itu rumah sendiri.
      Kalau akun kena limit atau algoritmanya berubah, kamu masih punya satu tempat yang tidak bisa
      diambil orang." = metafora + dua frasa kunci IDENTIK VERBATIM; dikuatkan 2026-10-25 10:06
      ("harus punya 'rumah' sendiri").
 #83 uky_quote4_berhenti_sebelum_terlambat -> DIPOSTING 7 Nov 2026 08:00 WIB. Frasa kunci kartu
      ("berhenti itu keputusan", "berhenti lebih awal", "proyek terbaik yang pernah saya selamatkan",
      "bukan kegagalan") TIDAK PERNAH muncul sebagai topic/baris pertama post mana pun (dicek ke
      seluruh 300 jadwal). Yang ada hanya tema bersaudara yang berbeda tesis: 2026-10-18 09:28
      (image, 'Belajar dari kegagalan sendiri') "kegagalan bukan lawan dari pertumbuhan", 2026-10-27
      09:36 "belajar dari proyek yang gagal", 2026-09-15 14:00 "hal yang saya sesali: terlalu lama
      diam karena takut salah". Sudut kartu (berhenti = keputusan menyelamatkan, bukan kegagalan)
      belum pernah jadi tema sendiri.

KARTU (standar 16 Sep 2026): square 1080x1080, varian PUTIH (255,255,255) + teks #122B63, header
"@ukyhandoko" + "1/1", rata kiri, satu blok tengah, tanpa separator, 0 CJK (dicek visual).

NADA (perintah Bos 5 Okt 2026): santai ("aku"/"kamu").
Distribusi tone dari hook_note di log (n=21): netral 13 / opini 4 / sanjungan 4.
  +netral    -> 14/4/4 = 63.6/18.2/18.2  dev 13.64
  +opini     -> 13/5/4 = 59.1/22.7/18.2  dev  6.36  <-- TERPILIH (4 kartu terakhir semua netral)
  +sanjungan -> 13/4/5 = 59.1/18.2/22.7  dev 15.45
Pilih opini (pernyataan berani + tanya setuju/nggak, sopan).

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 terakhir = 6 Nov 2026 08:00 WIB (item #77). Slot bebas berikutnya
(dicek API 13 Okt: 2026-11-07 SELURUH HARI kosong) = 7 Nov 2026 08:00 WIB.

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_berhenti_sebelum_terlambat"
FILENAME = "berhenti_sebelum_terlambat_putih.png"
SRC = os.path.join(ROOT, "uky4_output", FILENAME)
MEDIA_NAME = "uky_quote_berhenti_sebelum_terlambat.jpg"
TITLE = "uky Berhenti Sebelum Terlambat 2026-11-07"
TOPIC = "Berhenti itu keputusan, bukan kegagalan"      # 39 char (<=45)

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

BODY = (
    "Berhenti dari satu proyek itu bukan tanda kalah. Buat aku, itu justru salah satu keputusan "
    "bisnis paling mahal yang pernah aku ambil."
    + NL2 +
    "Proyek terbaik yang pernah aku selamatkan adalah proyek yang aku putuskan berhenti lebih "
    "awal. Tenaganya aku pindahkan ke kerjaan yang masih punya masa depan \u2014 sebelum semua "
    "orang kelelahan dan hasilnya jadi jelek."
)

HOOK = ("Setuju atau nggak: kadang berhenti itu justru langkah paling waras? "
        "Tulis pendapatmu di komentar \U0001F447")

SKIPS = [
    ("uky_quote4_otomatiskan_yang_membosankan", "otomatiskan_yang_membosankan_orange.png",
     "DUPLIKAT (hampir verbatim): kartu #78 \\\"Bukan alat mahal yang menghemat waktu saya, tapi "
     "menyerahkan pekerjaan yang bentuknya selalu sama ke mesin.\\\" Threads @ukyhandoko 2026-10-29 "
     "20:00 WIB (text, topic 'AI & Otomasi'): \\\"Pengalaman saya dengan otomatisasi: yang paling "
     "menghemat waktu bukan alat termahal, tapi yang menggantikan pekerjaan paling membosankan. "
     "Bagi saya itu bukan analisis data, tapi mengumpulkan laporan yang bentuknya selalu sama. "
     "Setelah itu diserahkan ke mesin, saya punya waktu untuk hal yang benar-benar berpikir.\\\" "
     "Kalimat kunci kartu (alat mahal, menghemat waktu, pekerjaan membosankan, bentuknya selalu "
     "sama, diserahkan ke mesin) sudah dipakai di akun yang sama. Tidak ada jadwal dibuat."),
    ("uky_quote4_tim_tahu_batasnya", "tim_tahu_batasnya_biru.png",
     "DUPLIKAT: kartu #79 \\\"Orang tidak butuh dimotivasi terus. Mereka butuh tahu apa tugasnya, "
     "sampai kapan, dan apa artinya selesai.\\\" Threads @ukyhandoko 2026-10-31 11:00 WIB (text, "
     "topic 'Tim & Karier'): \\\"orang tidak butuh dimotivasi terus, mereka butuh tahu batasnya. "
     "Ketika tugasnya jelas dan kapan selesainya jelas, sebagian besar orang bekerja dengan baik "
     "tanpa perlu ditunggui.\\\" Frasa pembuka identik + tesis sama. Tidak ada jadwal dibuat."),
    ("uky_quote4_tidak_itu_strategi", "tidak_itu_strategi_putih.png",
     "DUPLIKAT TEMA: kartu #80 bertesis 'pekerjaan yang diambil karena tidak enak menolak = "
     "penyesalan'. Tema menolak kerjaan sudah jadi topik/baris pertama 3x dalam <1 bulan: "
     "2026-10-15 09:35 'Saya pernah menolak klien karena briefnya tidak masuk akal secara bisnis', "
     "2026-10-17 09:28 'Saya pernah menolak proyek besar. Nilainya hampir sebulan penghasilan "
     "saya', 2026-10-17 09:29 'Tim kecil, permintaan banyak. Kalian menolak pekerjaan, atau "
     "menambah orang dulu?'; didukung 2026-10-31 12:51 ('saya lebih suka menolak satu pekerjaan "
     "tambahan') dan 2026-10-20 20:42 ('berani bilang tidak ke yang lain'). Kartu ke-6 bertema "
     "sama dalam 3 minggu = konten berulang. Tidak ada jadwal dibuat."),
    ("uky_quote4_ukur_yang_penting", "ukur_yang_penting_orange.png",
     "DUPLIKAT TEMA: kartu #81 ('Jumlah pengikut itu mudah dihitung. Yang lebih penting: berapa "
     "orang bertanya, berapa yang kembali, berapa yang bercerita ke orang lain'). Tema metrik "
     "ramai vs dampak sudah tayang: 2026-09-16 08:00 (image, topic 'Metrik vs dampak') 'Dulu saya "
     "juga ngejar angka: views, likes, followers ... yang paling berarti justru yang nggak "
     "kelihatan di dashboard'; 2026-09-15 23:00 'Follower itu bukan target utama'; 2026-10-25 "
     "09:00 (album) 'Pengikut banyak tidak otomatis jadi pelanggan'. Tidak ada jadwal dibuat."),
    ("uky_quote4_website_rumah_bukan_kost", "website_rumah_bukan_kost_biru.png",
     "DUPLIKAT (frasa kunci identik): kartu #82 \\\"Media sosial tempat menyapa. Website tempat "
     "orang mengenal kamu seutuhnya, dan tidak bisa diambil orang lain.\\\" Threads @ukyhandoko "
     "2026-10-04 17:00 WIB (text, topic 'Website'): \\\"media sosial itu tempat menyapa, website "
     "itu rumah sendiri. Kalau akun kena limit atau algoritmanya berubah, kamu masih punya satu "
     "tempat yang tidak bisa diambil orang.\\\" Metafora rumah + dua frasa kunci itu sama; "
     "dikuatkan 2026-10-25 10:06 ('harus punya \\\"rumah\\\" sendiri'). Tidak ada jadwal dibuat."),
]

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 #83 — tidak boleh pernah jadi topic/baris pertama post lain
FRASA83 = re.compile(
    r"berhenti itu keputusan|memutuskan berhenti|berhenti lebih awal|"
    r"proyek terbaik.{0,30}selamat|berhenti.{0,20}bukan kegagalan|"
    r"memilih berhenti|berhenti dari proyek", 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():
    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 == (255, 255, 255), f"bg {bg} bukan varian PUTIH seri uky4"

    cap = build_caption()
    print(f"CAPTION ({len(cap)} chars):")
    print(cap)
    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 #83")

    c = get_client()

    # cek tema: frasa kunci kartu #83 tidak boleh pernah jadi TOPIK/JUDUL post lain
    semua = fetch_uky(c, refresh=True)
    kemunculan, jadi_topik = [], []
    for d in semua:
        desc = d.get("description") or ""
        if not FRASA83.search(desc):
            continue
        kemunculan.append(d)
        baris1 = desc.strip().splitlines()[0] if desc.strip() else ""
        if FRASA83.search(baris1) or FRASA83.search(d.get("topic") or ""):
            jadi_topik.append(d)
    print(f"CEK TEMA frasa {FRASA83.pattern!r} di {len(semua)} jadwal Threads-uky: "
          f"{len(kemunculan)} kemunculan, {len(jadi_topik)} di antaranya jadi topik/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 "")[:30]), "|",
              (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 #83 belum pernah tayang.")

    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"

    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

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

    # UPDATE STATE + LOG (5 SKIPPED + 1 POSTED)
    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))
    now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z")
    for sid_id, fn, reason in SKIPS:
        log.append({
            "timestamp": now, "item_id": sid_id, "brand": "ukyhandoko", "source": "uky4_output",
            "status": "SKIPPED", "reason": reason,
            "filename": fn, "action": "skip, lanjut item berikutnya",
        })
    st["index"] = (st["index"] + len(SKIPS) + 1) % st["total"]
    log.append({
        "timestamp": now,
        "item_id": ITEM_ID, "brand": "ukyhandoko", "source": "uky4_output",
        "filename": f"{FILENAME} (kartu square 1080x1080, bg PUTIH + teks #122B63, varian uky4, "
                    f"standar 16 Sep - sesuai)",
        "topic": TOPIC,
        "slot_note": f"lane kartu uky4 lanjutan setelah 6 Nov 08:00 (item #77); {SLOT_WIB} - "
                     f"2026-11-07 SELURUH HARI kosong utk Threads-uky (dicek API 13 Okt). "
                     f"Item #78-#82 di-skip karena duplikat (2 hampir verbatim, 1 frasa kunci "
                     f"identik, 2 tema berulang).",
        "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": "opini (pernyataan berani + tanya setuju/nggak, nada santai 5 Okt 2026) "
                     "- dev minimum 6.36 dari pool n=21 (netral 13/opini 4/sanjungan 4); "
                     "4 kartu terakhir berturut-turut netral",
        "next_index": st["index"],
    })
    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())
