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

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

 #84 uky_quote4_kerja_rapi_klien_kembali -> DIPOSTING. Kartu: "Klien tidak menuntut hasil
      sempurna. Mereka cuma tidak suka dikejutkan. Rapi dan jujur jauh lebih mahal daripada
      janji bagus." Frasa kunci kartu -> 0 kemunculan di 295 jadwal: "kerja rapi" 0,
      "klien kembali" 0, "dikejutkan" 0, "janji bagus" 0, "hasil sempurna" 0, "ekspektasi" 0,
      "kejutan" 0, "progres" 0. Yang ada hanya tema SAUDARA dengan tesis berbeda:
      14 Okt 09:38 image 'Retensi klien = 90% komunikasi' (komunikasi menggantung = klien
      pergi), 23 Okt 12:38 text 'Agensi & Layanan' (disiplin membosankan: pencatatan, tenggat,
      laporan tepat waktu), 3 Nov 08:48 text 'Klien & Relasi' ("kurangi janji, perbanyak
      catatan" = rapi itu soal CATATAN kesepakatan, bukan soal hasil sempurna/dikejutkan).
      Sudut kartu (klien benci DIKEJUTKAN; rapi+jujur > janji bagus) belum pernah jadi tema
      sendiri -> lanjut. Risiko tumpang tindih RINGAN dilaporkan ke Bos.

 (Tidak ada SKIP di entri log run ini: audit berurutan berhenti di item pertama yang bersih,
  sesuai pola run 11-13 Okt. #85 `ai_ikut_bekerja_kamu_memutuskan` dan #86
  `konsisten_mengalahkan_viral` TERBUKTI duplikat hampir verbatim saat diaudit sekaligus —
  #85 vs 22 Okt 08:48 'AI & Otomasi' ("AI buat mengumpulkan bahan mentah... Bagian opini,
  pengalaman, dan keputusan tetap saya tulis sendiri"), 4 Okt 14:00 ("AI saya pakai untuk cari
  ide dan merapikan tulisan") dan 3 Nov 08:00 image ('AI alat, bukan pengganti otak' = item
  #70 antrean ini); #86 vs 8 Okt 20:17 '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"). Keduanya DIBIARKAN di antrean (index 85/86) dan
  akan di-skip resmi di run berikutnya; dilaporkan ke Bos supaya tahu seri kartu uky4
  praktis habis setelah #87.)

KARTU (standar 16 Sep 2026): square 1080x1080, varian ORANGE seri uky4 (bg (249,115,22)
+ teks putih — rotasi varian seri: #83 putih -> #84 orange), header "@ukyhandoko" + "1/1",
footer "@ukyhandoko • Founder StudioSoft", rata kiri, satu blok tengah, tanpa separator,
0 CJK (diverifikasi via vision_analyze).

NADA (perintah Bos 5 Okt 2026): santai ("aku"/"kamu").
Distribusi tone dari prefix hook_note di log (n=22): netral 13 / opini 5 / sanjungan 4.
  +netral    -> 14/5/4 = 60.9/21.7/17.4  dev 6.52
  +opini     -> 13/6/4 = 56.5/26.1/17.4  dev 6.96  <-- TERPILIH
  +sanjungan -> 13/5/5 = 56.5/21.7/21.7  dev 13.48
Dev netral vs opini nyaris seri (selisih 0.44, kurang dari bobot 1 entri) dan lane kartu
4 entri terakhir = netral,netral,netral,opini -> garis netral sudah terlalu padat di lane ini;
pilih opini (pernyataan berani + tanya setuju/nggak, sopan) yang juga menambal kuota 25%.

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 #83 = 7 Nov 2026 08:00 WIB -> lanjut 8 Nov 2026 08:00 WIB
(dicek API 14 Okt: 2026-11-08 SELURUH HARI kosong untuk Threads-uky; jendela 07:30-08:30 kosong).

Upload WP: DILEWATI — wp_image_map.json tidak punya entri `uky4_output/*` (seri uky memakai
media server openclaw-img, bukan WP).

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_kerja_rapi_klien_kembali"
FILENAME = "kerja_rapi_klien_kembali_orange.png"
SRC = os.path.join(ROOT, "uky4_output", FILENAME)
MEDIA_NAME = "uky_quote_kerja_rapi_klien_kembali.jpg"
TITLE = "uky Kerja Rapi Bikin Klien Kembali 2026-11-08"
TOPIC = "Rapi dan jujur kalahkan janji bagus"          # 35 char (<=45)

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

BODY = (
    "Klien itu nggak minta hasil yang sempurna. Yang bikin mereka pergi biasanya bukan "
    "kualitasnya, tapi rasa dikejutkan."
    + NL2 +
    "Pendapat yang mungkin nggak populer: janji bagus waktu nawarin jasa itu murah. Yang "
    "mahal adalah kerja rapi dan jujur pas proyeknya jalan \u2014 kabar buruk yang cepat "
    "jauh lebih murah daripada kejutan di akhir."
)

HOOK = "Setuju atau nggak? Tulis pendapat kamu di komentar \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 #84 — tidak boleh pernah jadi topic/baris pertama post lain
FRASA84 = re.compile(
    r"kerja rapi|klien kembali|dikejutkan|janji bagus|hasil sempurna|"
    r"rapi dan jujur|tidak menuntut hasil", 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 == (249, 115, 22), f"bg {bg} bukan varian ORANGE seri uky4"
    print("VARIAN: ORANGE (bg #F97316 + teks putih) - rotasi seri uky4 setelah #83 putih")

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

    c = get_client()

    # cek tema: frasa kunci kartu #84 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 FRASA84.search(desc):
            continue
        kemunculan.append(d)
        baris1 = desc.strip().splitlines()[0] if desc.strip() else ""
        if FRASA84.search(baris1) or FRASA84.search(d.get("topic") or ""):
            jadi_topik.append(d)
    print(f"CEK TEMA frasa {FRASA84.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 "")[: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 #84 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
    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")
    st["index"] = (st["index"] + 1) % st["total"]
    log.append({
        "timestamp": now,
        "item_id": ITEM_ID, "brand": "ukyhandoko", "source": "uky4_output",
        "filename": f"{FILENAME} (kartu square 1080x1080, varian ORANGE bg #F97316 + teks "
                    f"putih, seri uky4, standar 16 Sep - sesuai)",
        "topic": TOPIC,
        "slot_note": f"lane kartu uky4 lanjutan setelah item #83 (7 Nov 08:00); {SLOT_WIB} - "
                     f"2026-11-08 SELURUH HARI kosong utk Threads-uky (dicek API 14 Okt). "
                     f"Frasa kunci kartu 0 kemunculan di 295 jadwal korpus. Catatan utk Bos: "
                     f"#85 & #86 di antrean terbukti duplikat hampir verbatim, #87 masih perlu "
                     f"audit -> seri kartu uky4 praktis habis, siapkan seri baru.",
        "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 'pendapat yang mungkin nggak populer' + tanya "
                     "setuju/nggak, nada santai 5 Okt 2026) - dev netral 6.52 vs opini 6.96 "
                     "(nyaris seri) dari pool n=22 (netral 13/opini 5/sanjungan 4); lane kartu "
                     "4 entri terakhir netral,netral,netral,opini -> pilih opini",
        "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())
