#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""kunci_car10_okt.py — KUNCI carousel feed IG StudioSoft 6-31 Okt 2026 (perintah Bos 3 Okt 2026: "Ok sip kunci").

Yang dikerjakan:
  1. `--render`  : untuk tiap tanggal di MAPPING, tarik teks artikel studiosoft.id (via r.jina.ai, cache di
                   /tmp/art_cache), rakit 10 slide (cover + 8 isi + CTA) dengan ENGINE ss_carousel_lib dan
                   WARNA IKUT POLA HARIAN (L.warna_ke(TANGGAL, nomor_slide)) -> PNG di ss_ig_car10_okt/.
                   Sekaligus tulis caption ke /tmp/cap_<prefix>.txt.
  2. `--pasang`  : JPEG q85 -> openclaw-img/, gate 200 di KEDUA ingress, create album 08:30 WIB +
                   story mirror 08:40 WIB (kanvas_story), lalu verifikasi baca balik. Idempoten per slot.
  3. `--cek`     : cuma laporkan status tiap tanggal (ada/tidak, warna file, caption).

Semua warna ditentukan `ss_carousel_lib.warna_ke` (satu sumber kebenaran): 6 Okt genap -> slide 1 PUTIH,
7 Okt ganjil -> slide 1 NAVY, dst.
"""
import argparse
import datetime
import json
import os
import re
import subprocess
import sys
import time
from concurrent.futures import ThreadPoolExecutor

WS = "/home/mantapman/.hermes/profiles/atun/workspace"
IMG = os.path.join(WS, "openclaw-img")
OUT = os.path.join(WS, "ss_ig_car10_okt")
CACHE = "/tmp/art_cache"
sys.path.insert(0, WS)
import ss_carousel_lib as L  # noqa: E402
from repliz_client import get_client, ReplizError, IMG_BASE_URL  # noqa: E402
import pasang_car_generik as PG  # noqa: E402

WIB = datetime.timezone(datetime.timedelta(hours=7))
ACC = "6a38ac33c5ff4ce3a3231232"
NAVY = (18, 43, 99)
WHITE = (255, 255, 255)
TOTAL = 10
TOPIK = "Seri Industri StudioSoft: SEO & Digital Marketing"

# tanggal, slug artikel, prefix file, tag slide, hashtag caption
MAPPING = [
    ("2026-10-06", "konsultan-pemasaran-digital", "ss_car10_konsultan", "#konsultandigital"),
    ("2026-10-07", "tools-ai-untuk-bisnis-pilih-sesuai-kebutuhan", "ss_car10_toolsai", "#toolsai"),
    ("2026-10-08", "jasa-branding-surabaya-membangun-identitas-brand-kuat", "ss_car10_brandingsby", "#brandingsurabaya"),
    ("2026-10-09", "ai-dan-pekerjaan-digital-marketer-apa-yang-berubah", "ss_car10_aimarketer", "#aimarketing"),
    ("2026-10-10", "jasa-seo-malang", "ss_car10_seomalang", "#seomalang"),
    ("2026-10-11", "jasa-maintenance-wordpress-surabaya", "ss_car10_maintenance", "#maintenancewebsite"),
    ("2026-10-12", "bikin-konten-pakai-ai-boleh-asal-tahu-batasnya", "ss_car10_kontenai", "#kontenai"),
    ("2026-10-13", "digital-marketing-firma-hukum", "ss_car10_firmahukum", "#digitalmarketing"),
    ("2026-10-14", "cara-bikin-prompt-ai-yang-bagus-bukan-generik", "ss_car10_promptai", "#promptai"),
    ("2026-10-15", "marketing-maklon-minuman", "ss_car10_maklon", "#maklonminuman"),
    ("2026-10-16", "implementasi-ai-untuk-umkm", "ss_car10_aiumkm", "#aiumkm"),
    ("2026-10-17", "digital-marketing-bengkel-mobil", "ss_car10_bengkelmobil", "#bengkelmobil"),
    ("2026-10-18", "jasa-chatbot-whatsapp-bisnis", "ss_car10_chatbotwa", "#chatbotwhatsapp"),
    ("2026-10-19", "creative-agency-surabaya-solusi-kreatif-brand-berbeda", "ss_car10_creativeagency", "#creativeagency"),
    ("2026-10-20", "otomasi-bisnis-dengan-ai", "ss_car10_otomasiai", "#otomasibisnis"),
    ("2026-10-21", "digital-marketing-agency-surabaya-cara-memilih-partner", "ss_car10_agencypartner", "#digitalagency"),
    ("2026-10-22", "konsultan-ai-surabaya", "ss_car10_konsultanai", "#konsultanai"),
    ("2026-10-23", "jasa-automasi-marketing", "ss_car10_automasimarketing", "#automasimarketing"),
    ("2026-10-24", "branding-agency-surabaya", "ss_car10_brandingagency", "#brandingagency"),
    ("2026-10-25", "jasa-ai-agent-indonesia", "ss_car10_aiagent", "#aiagent"),
    ("2026-10-26", "retainer-digital-marketing", "ss_car10_retainer", "#retainer"),
    ("2026-10-27", "jasa-strategi-branding-campaign-surabaya", "ss_car10_brandingcampaign", "#brandingcampaign"),
    ("2026-10-28", "kelola-banyak-akun-media-sosial-dengan-ai-personal-assistant", "ss_car10_kelolaakun", "#kelolasosmed"),
    ("2026-10-29", "cara-memilih-jasa-seo-surabaya", "ss_car10_pilihseo", "#jasaseo"),
    ("2026-10-30", "berapa-biaya-jasa-pembuatan-website", "ss_car10_biayawebsite", "#jasawebsite"),
    ("2026-10-31", "masa-depan-seo-antara-manusia-ai-dan-google-di-2026", "ss_car10_seo2026", "#seo2026"),
]

BUANG = {"artikel terkait", "artikel terbaru", "arsip", "kategori", "layanan studiosoft", "komentar",
         "navigasi", "beranda", "hubungi kami", "postingan", "tags", "bagikan", "rekomendasi",
         "ulasan pelanggan", "trustindex", "baca juga", "baca juga:", "lihat juga", "simak juga"}
# seksi yang memuat jadwal/acara bertanggal lampau -> jangan dijadikan slide (konten jadi basi)
BASING = re.compile(r"(pukul \d{1,2}[.:]\d{2})|(20(2[0-5])\b)", re.I)


# ─────────────────────────── teks artikel ───────────────────────────
def ambil_teks(slug):
    os.makedirs(CACHE, exist_ok=True)
    p = os.path.join(CACHE, slug + ".md")
    if os.path.exists(p) and os.path.getsize(p) > 2000:
        return open(p, encoding="utf-8", errors="ignore").read()
    url = f"https://r.jina.ai/https://studiosoft.id/{slug}/"
    for a in range(3):
        r = subprocess.run(["curl", "-s", "--max-time", "200", url], capture_output=True, text=True)
        if r.returncode == 0 and len(r.stdout) > 2000:
            open(p, "w", encoding="utf-8").write(r.stdout)
            return r.stdout
        time.sleep(5 + 5 * a)
    return None


def _tabel_ke_teks(t):
    """Baris tabel markdown ("| Aspek | Agency |") diubah jadi kalimat butir yang terbaca.
    Tanpa ini, tabel tercetak mentah di slide (temuan 19 Okt 2026: deck 19/21/29 Okt)."""
    out = []
    for ln in t.splitlines():
        s = ln.strip()
        if s.startswith("|") and s.endswith("|") and s.count("|") >= 3:
            sel = [c.strip() for c in s.strip("|").split("|")]
            if all(re.fullmatch(r":?-{2,}:?", c) for c in sel if c):
                continue                      # baris pemisah | --- | --- |
            sel = [c for c in sel if c]
            if len(sel) >= 2:
                out.append(f"{sel[0]} — " + "; ".join(sel[1:]) + ".")
                continue
        out.append(ln)
    return "\n".join(out)


def bersih(t):
    t = _tabel_ke_teks(t)
    t = re.sub(r"(?m)^\s*#{1,6}\s*", "", t)      # heading markdown "### Pertanyaan" -> "Pertanyaan"
    t = re.sub(r"!\[[^\]]*\]\([^)]*\)", " ", t)
    t = re.sub(r"\[([^\]]+)\]\([^)]*\)", r"\1", t)
    t = t.replace("**", "").replace("*", "").replace("`", "")
    t = re.sub(r"\[[^\]]*\]", " ", t)
    t = t.replace("&nbsp;", " ").replace("&amp;", "&").replace("&#8217;", "'")
    t = re.sub(r"\s+", " ", t)
    return t.strip()


def kalimat(paragraf, maks=210):
    out = []
    for s in re.split(r"(?<=[.!?])\s+", paragraf):
        s = bersih(s)
        if len(s) < 25:
            continue
        if len(s) > maks:
            s = s[:maks].rsplit(" ", 1)[0] + "."
        out.append(s)
    return out


def tanpa_junk(kalimat_list):
    """Buang kalimat boilerplate/navigasi situs (mis. 'Skip to content Beranda Tentang ...').

    Bug 26 Okt 2026: intro artikel yang dibuka blok menu membuat `lead` slide cover
    tercetak sebagai sampah navigasi (deck retainer). JUNK dulu hanya dipakai di
    buat_caption, tidak di cover.
    """
    out = [k for k in kalimat_list
           if not any(j in k.lower() for j in JUNK) and not JUNK_RE.search(k)]
    return out or kalimat_list


def parse_artikel(slug):
    raw = ambil_teks(slug)
    if not raw:
        return None
    m = re.search(r"^Title:\s*(.+)$", raw, re.M)
    judul = bersih(m.group(1)) if m else slug.replace("-", " ")
    body = raw.split("Markdown Content:", 1)[-1]
    blok = re.split(r"^##\s+", body, flags=re.M)
    intro = bersih(blok[0])
    seksi = []
    for b in blok[1:]:
        lines = b.splitlines()
        if not lines:
            continue
        head = bersih(lines[0])
        isi = bersih(_tabel_ke_teks("\n".join(lines[1:])))   # tabel -> kalimat dulu, baru digabung
        if head.lower() in BUANG or len(isi) < 90:
            continue
        if BASING.search(isi):
            continue
        if len(head) > 70 or head.count("|") > 1:
            continue
        seksi.append((head, isi))
    return {"judul": judul, "intro": intro, "seksi": seksi}


def baris_judul(t, maks_kata=4):
    kata = t.upper().split()
    out, cur = [], []
    for w in kata:
        cur.append(w)
        if len(cur) >= maks_kata:
            out.append(" ".join(cur))
            cur = []
    if cur:
        out.append(" ".join(cur))
    return out[:4]


# ─────────────────────────── rakit + render ───────────────────────────
JUNK = ("skip to content", "beranda", "whatsapp", "artikel digital", "hubungi", "tentang layanan",
        "jasa seo professional", "social media web development", "cookie", "berlangganan",
        "baca juga", "baca:", "lihat juga", "simak juga")
JUNK_RE = re.compile(r"(\(\d{1,2}/\d{1,2}/\d{4}\))|(\b\d{1,2} (januari|februari|maret|april|mei|juni|juli|agustus|september|oktober|november|desember) \d{4}\b)", re.I)


def buat_caption(art, slug, tag):
    """Caption 2-3 paragraf dari isi artikel: buang kalimat navigasi/boilerplate, pilih kalimat utuh."""
    kandidat = []
    for s in kalimat(art["intro"]):
        kandidat.append(s)
    for h, isi in art["seksi"]:
        for s in kalimat(isi)[:2]:
            kandidat.append(s)
    bersih_ = []
    for s in kandidat:
        low = s.lower()
        if len(s) < 55 or any(j in low for j in JUNK) or JUNK_RE.search(s):
            continue
        if s not in bersih_:
            bersih_.append(s)

    link = f"https://studiosoft.id/{slug}/"
    hs = [tag, "#digitalmarketing", "#seo"]
    if re.search(r"\bAI\b", art["judul"]):
        hs.insert(1, "#ai")
    hs.append("#studiosoft")
    cta = "Menurut Anda, bagian mana yang paling relevan untuk bisnis Anda? Tulis di komentar."

    if len(bersih_) >= 3:
        cap = f"{bersih_[0]}\n\n{bersih_[1]}\n{bersih_[2]}\n\nSelengkapnya di {link}\n\n{cta}\n\n{' '.join(hs)}"
    elif bersih_:
        cap = f"{bersih_[0]}\n\nSelengkapnya di {link}\n\n{cta}\n\n{' '.join(hs)}"
    else:
        cap = (f"Panduan lengkapnya ada di artikel terbaru kami: {link}\n\n{cta}\n\n{' '.join(hs)}")
    if len(cap) > 1500:
        cap = f"{bersih_[0]}\n\nSelengkapnya di {link}\n\n{cta}\n\n{' '.join(hs)}"
    return cap


def mk(num, tag, tgl):
    if L.warna_ke(tgl, num) == "putih":
        return L.Slide(num, TOTAL, tag, bg=WHITE, fg=NAVY, dot_color=NAVY), L.AKSEN_PUTIH
    return L.Slide(num, TOTAL, tag, bg=NAVY, fg=WHITE), L.SKY


def rakit(entri):
    tgl_s, slug, prefix, tag = entri
    tgl = datetime.date.fromisoformat(tgl_s)
    art = parse_artikel(slug)
    if not art:
        return None, "teks artikel gagal diambil"
    intro_k = tanpa_junk(kalimat(art["intro"]))
    seksi = art["seksi"]
    if len(seksi) < 4 or len(intro_k) < 1:
        return None, f"struktur artikel tidak cukup (seksi={len(seksi)})"

    # 8 slide isi: satu seksi satu slide (3-4 butir). Kalau seksi kurang dari 8, seksi TERPANJANG
    # dibelah dua ("lanjutan") supaya tetap 10 slide tanpa mengulang judul yang sama berkali-kali.
    isi_seksi = [(h, kalimat(i)) for h, i in seksi]
    n_butir = 4 if len(isi_seksi) <= 6 else 3
    unit = [(h, ks[:n_butir], ks[n_butir:]) for h, ks in isi_seksi]
    pilih = [(h, ks) for h, ks, _ in unit]
    if len(pilih) < 8:
        tambah, urut_sisa = {}, sorted(range(len(unit)), key=lambda i: -len(unit[i][2]))
        for i in urut_sisa:
            if len(pilih) + len(tambah) >= 8:
                break
            if len(unit[i][2]) >= 2:
                tambah[i] = unit[i][2][:n_butir]
        pilih = []
        for i, (h, ks, sisa) in enumerate(unit):
            pilih.append((h, ks))
            if i in tambah:
                pilih.append((h + " (lanjutan)", tambah[i]))
    isi_slides = pilih[:8]

    os.makedirs(OUT, exist_ok=True)
    files = []

    s, ac = mk(1, tag, tgl)
    s.title([(baris_judul(art["judul"])[0], ac)] + baris_judul(art["judul"])[1:], after=40)
    s.lead(intro_k[0], after=26)
    if len(intro_k) > 1:
        s.para(intro_k[1], size=29)
    files.append(s.render(f"{prefix}_01.png", OUT, quiet=True))

    for i, (head, ks) in enumerate(isi_slides, start=2):
        s, ac = mk(i, tag, tgl)
        s.title(baris_judul(head, maks_kata=5), after=32)
        s.bullets([(k, "") for k in ks[:3]], name_size=32, desc_size=28, gap=24)
        files.append(s.render(f"{prefix}_{i:02d}.png", OUT, quiet=True))

    s, ac = mk(10, tag, tgl)
    s.title([("LANGKAH", ac), "BERIKUTNYA"], after=32)
    s.bullets([
        ("Lihat panduannya", "penjelasan lengkap ada di artikel ini."),
        ("Cek kondisi bisnis Anda", "mulai dari satu masalah yang paling mengganggu."),
        ("Butuh dibantu", "tim StudioSoft bisa memetakan prioritasnya."),
    ], name_size=32, desc_size=29, gap=26)
    s.para("www.studiosoft.id atau kirim pesan lewat DM.", size=28, color=ac)
    files.append(s.render(f"{prefix}_10.png", OUT, quiet=True))

    # caption
    cap = buat_caption(art, slug, tag)
    open(f"/tmp/cap_{prefix}.txt", "w", encoding="utf-8").write(cap)
    return {"tgl": tgl_s, "slug": slug, "prefix": prefix, "tag": tag,
            "judul": art["judul"], "files": len(files), "caption": len(cap)}, ""


# ─────────────────────────── pasang ───────────────────────────
def gate(nama):
    for ip in PG.INGRESS:
        code = subprocess.run(["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "--max-time", "30",
                               "--resolve", f"{PG.HOST}:443:{ip}",
                               f"https://{PG.HOST}/openclaw-img/{nama}"],
                              capture_output=True, text=True).stdout.strip()
        if code != "200":
            return nama, ip, code
    return None


def pasang(entri, go=False, occ=None):
    from PIL import Image
    tgl_s, slug, prefix, tag = entri
    tgl = datetime.date.fromisoformat(tgl_s)
    folder = OUT
    for i in range(1, 11):
        im = Image.open(os.path.join(folder, f"{prefix}_{i:02d}.png")).convert("RGB")
        assert im.size == (1080, 1350), f"{prefix}_{i:02d} {im.size}"
        im.save(os.path.join(IMG, f"{prefix}_{i:02d}.jpg"), "JPEG", quality=85, optimize=True)

    with ThreadPoolExecutor(max_workers=8) as ex:
        hasil = list(ex.map(gate, [f"{prefix}_{i:02d}.jpg" for i in range(1, 11)]))
    gagal = [h for h in hasil if h]
    if gagal:
        return f"GATE GAGAL {gagal[:3]}"
    if not go:
        return "siap (dry)"

    at = f"{tgl_s}T01:30:00.000Z"          # 08:30 WIB
    story_at = f"{tgl_s}T01:40:00.000Z"     # 08:40 WIB
    cap = open(f"/tmp/cap_{prefix}.txt", encoding="utf-8").read().strip()

    if occ is not None and at[:13] in occ:   # idempoten: slot 08:30 tanggal itu sudah terisi
        return "⏭️ SKIP slot 08:30 sudah terisi"
    c = get_client()

    album = {"accountId": ACC, "title": f"Carousel IG SS — {slug}", "topic": TOPIK, "type": "album",
             "description": cap,
             "medias": [{"url": f"{IMG_BASE_URL}/openclaw-img/{prefix}_{i:02d}.jpg", "type": "image",
                         "alt": f"Slide {i}"} for i in range(1, 11)],
             "scheduleAt": at, "replies": [], "additionalInfo": {"isShareToFeed": True}}
    r1 = c.post("/public/schedule", album)
    sid1 = r1.get("scheduleId") or r1.get("_id") or r1.get("id")
    nama_story = PG.kanvas_story(f"{prefix}_01.jpg")
    ok = gate(nama_story)
    if ok:
        return f"GATE STORY GAGAL {ok}"
    stori = {"accountId": ACC, "title": f"Story IG SS mirror {prefix}", "topic": TOPIK, "type": "story",
             "description": "", "medias": [{"url": f"{IMG_BASE_URL}/openclaw-img/{nama_story}",
                                            "type": "image", "alt": "Slide 1"}],
             "scheduleAt": story_at, "replies": []}
    r2 = c.post("/public/schedule", stori)
    sid2 = r2.get("scheduleId") or r2.get("_id") or r2.get("id")
    d1 = c.get(f"/public/schedule/{sid1}"); d1 = d1.get("data") or d1
    d2 = c.get(f"/public/schedule/{sid2}"); d2 = d2.get("data") or d2
    return (f"✅ album {sid1[-6:]} {d1.get('status')} {d1.get('scheduleAt')} media={len(d1.get('medias') or [])} "
            f"| story {sid2[-6:]} {d2.get('status')} {d2.get('scheduleAt')} "
            f"| caption {len(cap)} char | file {nama_story}")


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--render", action="store_true")
    ap.add_argument("--caption-saja", action="store_true",
                    help="cuma tulis ulang caption /tmp/cap_<prefix>.txt dari artikel (tanpa render slide)")
    ap.add_argument("--pasang", action="store_true")
    ap.add_argument("--go", action="store_true")
    ap.add_argument("--mulai", default=None)
    ap.add_argument("--sampai", default=None)
    a = ap.parse_args()

    entri_list = [e for e in MAPPING
                  if (not a.mulai or e[0] >= a.mulai) and (not a.sampai or e[0] <= a.sampai)]
    if a.caption_saja:
        for tgl_s, slug, prefix, tag in entri_list:
            art = parse_artikel(slug)
            if not art:
                print(f"{tgl_s} {prefix:26s} ⛔ artikel gagal dibaca")
                continue
            cap = buat_caption(art, slug, tag)
            open(f"/tmp/cap_{prefix}.txt", "w", encoding="utf-8").write(cap)
            print(f"{tgl_s} {prefix:26s} caption {len(cap)} char | baris1: {cap.splitlines()[0][:80]}")
    if a.render:
        for e in entri_list:
            info, err = rakit(e)
            print(f"{e[0]} {e[2]:26s} " + (f"ok render {info['files']}/10 · caption {info['caption']} char · "
                                           f"{info['judul'][:52]}" if info else f"⛔ {err}"))
    if a.pasang:
        occ = set()
        try:
            c0 = get_client()
            page = 1
            while page <= 80:
                r = c0.get_schedules(page=page, limit=100)
                docs = r.get("docs") or []
                for d in docs:
                    if d.get("accountId") == ACC:
                        occ.add(str(d.get("scheduleAt") or "")[:13])
                if len(docs) < 100:
                    break
                page += 1
            print(f"slot terisi akun IG SS: {len(occ)} (termasuk jam lain)")
        except Exception as ex:
            print("⚠️ gagal baca slot terisi:", ex)
        for e in entri_list:
            try:
                hasil = pasang(e, go=a.go, occ=occ)
                if hasil.startswith("✅"):
                    occ.add(f"{e[0]}T01:30")
                print(f"{e[0]} {e[2]:26s} {hasil}")
            except ReplizError as ex:
                print(f"{e[0]} {e[2]:26s} ⛔ ReplizError: {ex}")
            except Exception as ex:
                print(f"{e[0]} {e[2]:26s} ⛔ {type(ex).__name__}: {ex}")


if __name__ == "__main__":
    main()
