#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""SERI StudioSoft #2 — SEO, insight sosmed, analitik, dan data marketing untuk PROPERTI,
plus vertikal RETAIL dan KEUANGAN.

17 topik x 3 kanal (Threads / Facebook / LinkedIn StudioSoft), 1 post/hari/kanal.
Link tiap post diarahkan ke artikel yang relevan (8 artikel yang tayang 2 Okt 2026).
Mode: preview [N] | run [N]  (idempoten lewat ss_seri_vertikal2_state.json)
"""
import json
import os
import re
import sys
from datetime import datetime, timedelta, timezone

from repliz_client import get_client, ReplizError

WIB = timezone(timedelta(hours=7))
SLOTS_WIB = [5, 8, 11, 14, 17, 20, 23]
STATE = "ss_seri_vertikal2_state.json"

L_PROP = "https://studiosoft.id/digital-marketing-bisnis-properti/"
L_SEO = "https://studiosoft.id/seo-untuk-bisnis-properti/"
L_SOS = "https://studiosoft.id/insight-media-sosial-bisnis-properti/"
L_ANA = "https://studiosoft.id/analitik-digital-bisnis-properti/"
L_DATA = "https://studiosoft.id/data-marketing-bisnis-properti/"
L_RET = "https://studiosoft.id/digital-marketing-bisnis-retail/"
L_KEU = "https://studiosoft.id/digital-marketing-bisnis-keuangan/"

TARGETS = [
    {"key": "threads_ss2", "label": "Threads StudioSoft", "account": "6a335b9ec5ff4ce3a3225c2a", "kind": "threads"},
    {"key": "fb_ss2", "label": "Facebook StudioSoft", "account": "6a334952c5ff4ce3a3225941", "kind": "fb"},
    {"key": "li_ss2", "label": "LinkedIn StudioSoft", "account": "6a334c49c5ff4ce3a32259c9", "kind": "li"},
]

S = [
    dict(nama="SEO properti: struktur halaman per lokasi", link=L_SEO, tag="#seo #properti #seolokal",
         hook="Situs properti yang menang hampir selalu punya satu hal yang jarang dimiliki penjual lain: halaman terpisah untuk tiap lokasi.",
         isi1="Halaman hub per jenis properti, halaman per area, lalu halaman per unit.",
         isi2="Tautkan unit ke area, dan area ke hub, supaya mesin paham mana halaman utama.",
         kw="rumah dijual nama kecamatan, cluster nama proyek, kontrakan nama daerah",
         fb_par="Selama ini penjual properti membuat satu halaman katalog berisi semua unit. Halaman seperti itu hanya bisa relevan untuk satu pencarian, sementara calon pembeli mencarinya lewat puluhan kombinasi jenis dan lokasi.",
         li_par="Di bisnis properti, struktur situs menentukan seberapa banyak pintu masuk yang bisa Anda bangun. Satu halaman per lokasi membuat setiap area punya pintunya sendiri.",
         cta="berapa halaman lokasi yang sudah Anda miliki sekarang?"),
    dict(nama="SEO properti: pola kata kunci jenis + lokasi", link=L_SEO, tag="#seo #katakunci #properti",
         hook="Kata kunci properti hampir selalu berbentuk dua bagian: jenis properti dan lokasi. Siapa pun yang menguasai kombinasi itu, dialah yang ditemukan.",
         isi1="Tiga lapis: jenis + kota, jenis + kecamatan, lalu nama proyek atau nama jalan.",
         isi2="Jangan kejar kata kunci volume besar tanpa lokasi, pesaingnya portal raksasa.",
         kw="jenis properti + kota, jenis + kecamatan, nama proyek, nama jalan",
         fb_par="Kata kunci seperti rumah dijual jumlah pencariannya besar tapi bersaing dengan seluruh negeri. Kata kunci bertingkat lokasi jumlahnya lebih kecil, tapi orang yang mengetiknya sudah menetapkan lokasi dan anggaran.",
         li_par="Pemilihan kata kunci untuk properti lebih dekat ke pemetaan wilayah penjualan daripada ke pemasaran massal. Tiap area punya halaman dan pesannya sendiri.",
         cta="area mana yang paling banyak menghasilkan bagi bisnis Anda selama ini?"),
    dict(nama="SEO properti: schema dan kecepatan halaman", link=L_SEO, tag="#seo #teknis #properti",
         hook="Dua hal teknis yang paling menentukan di situs properti: data terstruktur dan berat halaman karena foto.",
         isi1="Schema Product atau RealEstateListing plus Offer membuat harga dan lokasi terbaca mesin.",
         isi2="Foto ponsel 4 MB dilawan dengan kompresi dan WebP, bukan dengan menambah server.",
         kw="data terstruktur listing, core web vitals situs properti, kompresi galeri foto",
         fb_par="Situs properti hampir selalu berat karena galeri foto, dan halaman yang berat lebih jarang muncul di peringkat atas sekaligus lebih cepat ditinggalkan pengunjung.",
         li_par="Untuk tim properti, perbaikan teknis seperti data terstruktur dan kecepatan halaman punya efek ganda: menaikkan peringkat sekaligus menahan pengunjung lebih lama.",
         cta="sudah pernah cek kecepatan halaman unit Anda?"),
    dict(nama="SEO properti: Google Business Profile dan Maps", link=L_SEO, tag="#seolokal #maps #properti",
         hook="Sebagian besar pencarian properti berujung pada pertanyaan yang sama: yang dekat sini mana.",
         isi1="Kantor pemasaran, rumah contoh, dan lokasi proyek sebaiknya punya profil yang dirawat.",
         isi2="Review diminta setelah serah terima, saat pengalaman pembeli masih segar.",
         kw="rumah contoh dekat sini, kantor pemasaran nama proyek, agen properti terdekat",
         fb_par="Profil Google yang lengkap dengan foto proyek terbaru dan jawaban atas pertanyaan umum membuat calon pembeli datang tanpa harus menghubungi agen lebih dulu.",
         li_par="Bagi agen dan developer, keberadaan di peta adalah titik temu antara pencarian digital dan kunjungan fisik, dan itu sering jadi prospek paling berkualitas.",
         cta="seberapa lengkap profil lokasi bisnis Anda sekarang?"),
    dict(nama="Insight sosmed properti: metrik yang layak dibaca", link=L_SOS, tag="#sosmed #insight #properti",
         hook="Akun properti biasanya ramai tayangan dan sepi percakapan. Penyebabnya bukan kontennya, tapi metrik yang dipantau.",
         isi1="Yang dibaca: percakapan masuk, simpan, klik tautan, dan jangkauan lokal.",
         isi2="Jumlah follower dan like tetap dicatat, tapi bukan dasar keputusan.",
         kw="save rate konten properti, dm masuk, jangkauan lokal, kunjungan profil",
         fb_par="Untuk properti, jumlah like tidak memberi tahu apa pun soal minat beli. Yang memberi tahu adalah berapa orang mengirim pesan dan berapa yang menyimpan postingan untuk dilihat lagi nanti.",
         li_par="Metrik konten properti sebaiknya dipilih yang dekat dengan keputusan: percakapan, simpan, dan kunjungan halaman unit, bukan paparan semata.",
         cta="metrik apa yang selama ini Anda pantau di akun properti?"),
    dict(nama="Insight sosmed properti: format konten yang menghasilkan DM", link=L_SOS, tag="#konten #sosmed #properti",
         hook="Enam format konten properti yang berulang kali menghasilkan percakapan, dan semuanya bukan konten estetik.",
         isi1="Tur video satu unit, progres pembangunan, dan harga terbuka.",
         isi2="Lalu jawaban pertanyaan berulang, sebelum-sesudah, dan testimoni serah terima.",
         kw="video tur rumah, progres proyek, konten harga properti, jawab pertanyaan pembeli",
         fb_par="Video tur satu menit yang urut ruang demi ruang menghemat banyak kunjungan yang sebenarnya tidak cocok, dan itu menguntungkan penjual maupun pembeli.",
         li_par="Konten properti yang berfungsi bukan yang indah, tapi yang menjawab keraguan. Progres, legalitas, dan perhitungan cicilan ada di urutan teratas.",
         cta="format konten mana yang paling banyak membuat orang menghubungi Anda?"),
    dict(nama="Insight sosmed properti: kanal mana untuk segmen mana", link=L_SOS, tag="#sosmed #kanal #properti",
         hook="Instagram, TikTok, Facebook, dan LinkedIn tidak melayani pembeli yang sama di bisnis properti.",
         isi1="IG dan Threads untuk pembeli akhir, TikTok untuk jangkauan tur dan progres.",
         isi2="Facebook untuk komunitas lokal, LinkedIn untuk ruko, gudang, dan kantor.",
         kw="pemasaran properti instagram, tiktok tur rumah, facebook grup jual beli daerah, linkedin sewa gudang",
         fb_par="Banyak agen membuang tenaga dengan konten yang sama di semua kanal. Padahal tiap kanal punya pembeli dengan pertanyaan yang berbeda.",
         li_par="Untuk properti komersial, keputusan diambil di ruang rapat perusahaan. Di situ LinkedIn bekerja lebih baik daripada kanal visual.",
         cta="kanal mana yang paling produktif untuk properti Anda?"),
    dict(nama="Analitik properti: event yang harus dicatat", link=L_ANA, tag="#analitik #ga4 #properti",
         hook="Sebelum membaca laporan apa pun, pastikan lima kejadian ini tercatat di website properti Anda.",
         isi1="Klik WhatsApp, klik telepon, kirim formulir, minta brosur, dan buka simulasi cicilan.",
         isi2="Tanpa lima itu, laporan apa pun hanya menampilkan kunjungan yang tidak bisa dipakai memutuskan.",
         kw="event konversi whatsapp, tracking formulir properti, ga4 properti",
         fb_par="Kebanyakan pemilik bisnis properti punya akun analitik yang tidak pernah dibuka, karena isinya jumlah kunjungan yang tidak berhubungan dengan penjualan.",
         li_par="Pengukuran di bisnis properti sebaiknya dimulai dari definisi prospek, bukan dari alat. Setelah prospek didefinisikan, alatnya mengikuti.",
         cta="apa definisi prospek di bisnis Anda, dan apakah tercatat?"),
    dict(nama="Analitik properti: Search Console untuk pencarian lokasi", link=L_ANA, tag="#analitik #searchconsole #properti",
         hook="Search Console memberi satu hal yang tidak diberikan alat lain: kata kunci yang benar-benar diketik orang sebelum menemukan Anda.",
         isi1="Lihat permintaan yang memuat nama daerah, nama proyek, atau jumlah kamar.",
         isi2="Tayangan tinggi tapi klik rendah biasanya berarti judul halaman perlu diperbaiki.",
         kw="search console properti, kata kunci lokal, ctr judul halaman",
         fb_par="Kalau sebagian besar tayangan datang dari luar kota yang Anda layani, artinya halaman Anda masih terlalu umum dan perlu dibuat lebih spesifik per area.",
         li_par="Untuk bisnis dengan cakupan wilayah, laporan pencarian adalah peta permintaan pasar. Isinya lebih jujur daripada asumsi tim internal.",
         cta="kota mana yang paling banyak mencari properti Anda?"),
    dict(nama="Analitik properti: dashboard enam angka", link=L_ANA, tag="#analitik #dashboard #properti",
         hook="Rapat pemasaran properti tidak perlu puluhan metrik. Enam angka ini cukup untuk memutuskan.",
         isi1="Percakapan masuk, sumbernya, kunjungan lokasi, penawaran, transaksi, waktu balas pertama.",
         isi2="Biaya per prospek per kanal yang menentukan ke mana anggaran digeser.",
         kw="dashboard pemasaran properti, biaya per lead, waktu respon",
         fb_par="Yang sering terlewat justru angka paling murah diperbaiki: berapa lama rata-rata tim menjawab percakapan baru. Padahal calon pembeli menghubungi beberapa penjual sekaligus.",
         li_par="Laporan yang berguna menjawab empat pertanyaan: berapa prospek, dari mana, mana yang paling efisien, dan apa yang diubah bulan depan.",
         cta="berapa lama rata-rata waktu balas tim Anda ke calon pembeli?"),
    dict(nama="Data marketing properti: pakai ulang daftar lead lama", link=L_DATA, tag="#datamarketing #crm #properti",
         hook="Aset paling murah di bisnis properti adalah daftar orang yang pernah bertanya, dan biasanya daftar itu tidak pernah dibuka lagi.",
         isi1="Kontak WhatsApp lama, pengunjung pameran, dan pengunjung yang pernah lihat unit tertentu.",
         isi2="Mereka sudah mengenal Anda, jadi biaya menghubungi ulang jauh lebih kecil.",
         kw="database prospek properti, follow up lead lama, crm properti",
         fb_par="Mencari orang baru setiap bulan jauh lebih mahal daripada menyapa kembali orang yang pernah menanyakan unit, apalagi yang butuh properti dengan kriteria spesifik.",
         li_par="Dalam bisnis dengan transaksi bernilai besar, daftar prospek yang terawat adalah aset jangka panjang, bukan sisa percakapan lama.",
         cta="kapan terakhir kali Anda menyapa ulang daftar prospek lama?"),
    dict(nama="Data marketing properti: segmen lokasi, anggaran, tahap", link=L_DATA, tag="#datamarketing #segmentasi #properti",
         hook="Tiga variabel yang cukup untuk membentuk segmen properti: lokasi yang diinginkan, rentang anggaran, dan tahap kesiapan.",
         isi1="Pertanyaan sederhana soal rencana beli memisahkan daftar jadi siap dan belum siap.",
         isi2="Daftar yang siap dikejar, daftar yang belum siap diberi konten edukasi rutin.",
         kw="segmentasi prospek properti, lead scoring sederhana, tahap kesiapan beli",
         fb_par="Mengirim unit di area yang tidak dicari orang biasanya berakhir dengan pesan tidak dibalas. Sebaliknya, satu unit yang cocok dengan kriteria mereka hampir selalu mendapat respons.",
         li_par="Segmentasi membuat tim penjualan bekerja lebih pendek dan lebih terarah, dan membuat anggaran pemasaran bisa dipertanggungjawabkan per kelompok.",
         cta="kriteria apa yang paling sering membuat orang membeli di tempat Anda?"),
    dict(nama="Data marketing properti: retargeting pengunjung unit", link=L_DATA, tag="#retargeting #iklan #properti",
         hook="Pengunjung yang pernah membuka halaman unit adalah audiens dengan niat paling jelas, dan biasanya mereka tidak pernah disapa lagi.",
         isi1="Kelompokkan pengunjung unit tertentu dan pengunjung halaman simulasi cicilan.",
         isi2="Orang yang pernah menonton separuh video tur juga layak dimasukkan.",
         kw="retargeting properti, custom audience iklan, iklan ulang pengunjung",
         fb_par="Orang yang melihat unit biasanya baru siap beberapa pekan kemudian. Iklan ulang ke orang yang sudah mengenal Anda jauh lebih murah daripada iklan ke orang baru.",
         li_par="Pemasaran ulang membuat biaya per prospek turun tanpa menambah jangkauan baru, dan hasilnya lebih mudah diukur karena kelompoknya jelas.",
         cta="sudah pernah mencoba memasarkan ulang ke pengunjung halaman unit?"),
    dict(nama="Retail: hitung dari margin, bukan omzet", link=L_RET, tag="#retail #umkm #margin",
         hook="Iklan retail paling sering gagal bukan karena iklannya buruk, tapi karena biaya per pembeli baru melebihi laba dari pembelian pertama.",
         isi1="Hitung laba per transaksi dan biaya per pembeli baru sebelum menambah anggaran.",
         isi2="Kalau angkanya belum sehat, perbaiki retensi dulu sebelum menambah iklan.",
         kw="biaya akuisisi pelanggan retail, margin transaksi, roas toko",
         fb_par="Penjualan yang naik belum tentu untung. Di retail yang marginnya tipis, biaya iklan dan diskon bisa menghabiskan laba tanpa terasa di laporan penjualan.",
         li_par="Untuk peritel, pertanyaan pertama sebaiknya bukan seberapa besar anggaran iklan, tapi berapa lama pelanggan baru kembali menutup biaya mendapatkannya.",
         cta="apa yang lebih sering Anda periksa, omzet atau laba per transaksi?"),
    dict(nama="Retail: toko fisik, local SEO, dan retensi", link=L_RET, tag="#retail #seolokal #retensi",
         hook="Untuk retail, tiga kanal dengan biaya paling sehat adalah profil Google lokal, katalog yang sinkron, dan pelanggan lama.",
         isi1="Halaman per cabang mengumpulkan pencarian lokal yang tidak bisa direbut kompetitor jauh.",
         isi2="Pengingat waktu pembelian berikutnya biasanya mengalahkan iklan ke orang baru.",
         kw="pesan sembako antar nama daerah, apotek 24 jam nama kota, kue nama kota",
         fb_par="Konsumen retail memutuskan cepat dan biasanya dari jarak. Kalau informasi harga, stok, dan lokasi siap, mereka datang; kalau tidak, mereka pindah ke toko berikutnya di hasil pencarian.",
         li_par="Retail yang menang dalam jangka panjang adalah yang datanya akurat dan pelanggan lamanya terawat, bukan yang iklannya paling besar.",
         cta="berapa persen penjualan Anda datang dari pelanggan lama?"),
    dict(nama="Keuangan: kepatuhan dan kepercayaan", link=L_KEU, tag="#keuangan #kepatuhan #bpr",
         hook="Di jasa keuangan, materi pemasaran yang menjanjikan hasil cepat bukan cuma berisiko dari sisi aturan, tapi juga merusak kepercayaan.",
         isi1="Sebut biaya, denda, syarat, dan risiko apa adanya di halaman produk.",
         isi2="Tampilkan izin dan nomor registrasi, karena itu yang pertama dicari orang.",
         kw="pinjaman online terdaftar, syarat kredit usaha, biaya total pinjaman",
         fb_par="Masyarakat makin hati-hati terhadap penawaran keuangan, dan kehati-hatian itu wajar. Kejelasan biaya dan legalitas justru membuat lembaga yang jujur terlihat berbeda.",
         li_par="Di sektor keuangan, kepatuhan dan pemasaran sebaiknya bekerja sejak perencanaan konten, bukan sebagai pemeriksaan terakhir sebelum tayang.",
         cta="menurut Anda, apa yang paling membuat orang percaya pada lembaga keuangan?"),
    dict(nama="Keuangan: halaman per kebutuhan dan simulasi", link=L_KEU, tag="#keuangan #simulasi #asuransi",
         hook="Orang tidak mencari nama produk kredit. Mereka mencari kebutuhan: kredit usaha kecil, asuransi kesehatan keluarga, dana pendidikan anak.",
         isi1="Susun halaman dari kebutuhan, baru sebutkan nama produknya di dalam.",
         isi2="Simulasi cicilan dan kalkulator kebutuhan dana membuat orang bertahan di halaman.",
         kw="simulasi kredit rumah, asuransi kesehatan keluarga, dana pendidikan anak",
         fb_par="Halaman yang menjelaskan biaya total, syarat dokumen, dan langkah pengajuan dari awal sampai dana masuk menghilangkan hambatan terbesar calon nasabah.",
         li_par="Untuk pemasaran keuangan, konten edukasi yang bisa dipakai calon nasabah mengambil keputusan memberi hasil lebih tahan lama daripada promosi potongan biaya.",
         cta="kebutuhan mana yang paling banyak ditanyakan calon nasabah Anda?"),
]


def cta_bersih(s):
    t = re.sub(r"^(menurut\s+(anda|kamu)\s*,?\s*)", "", s["cta"].strip(), flags=re.I)
    return t[0].upper() + t[1:]


def caption_threads(s, idx=0):
    q = cta_bersih(s)
    varian = [
        f"{s['hook']}\n\n• {s['isi1']}\n\n{s['link']}\n\n{q} Tulis di komentar ya.\n\n{s['tag']}",
        f"{s['hook']}\n\nMulai dari sini: {s['isi1'].lower()}\n\nSelengkapnya: {s['link']}\n\n{q} Cerita di komentar ya.\n\n{s['tag']}",
        f"{s['hook']}\n\nYang perlu dibenahi lebih dulu: {s['isi1'].lower()}\n\nPanduan lengkapnya: {s['link']}\n\n{q} Tulis pendapatmu di komentar.\n\n{s['tag']}",
    ]
    return varian[idx % len(varian)]


def reply_threads(s):
    return (f"Kata kunci yang biasanya dibidik: {s['kw']}.\n\n"
            f"Satu lagi yang sering kelewat: {s['isi2'].lower()}\n\n"
            f"Kalau kamu pernah mengalaminya, cerita dikit di komentar biar yang lain ikut belajar.")


def caption_fb(s, idx=0):
    q = cta_bersih(s)
    pembuka = [s["hook"], f"Satu hal yang sering kelewat: {s['isi1'].lower()}",
               f"{s['hook']} Pola ini kami temui berulang di beberapa klien."][idx % 3]
    return (f"{pembuka}\n\n{s['fb_par']}\n\n"
            f"Yang perlu dibenahi lebih dulu:\n• {s['isi1']}\n• {s['isi2']}\n\n"
            f"Versi lengkapnya ada di artikel ini:\n{s['link']}\n\n"
            f"{q} Tulis di kolom komentar, saya bantu jawab satu per satu.\n\n"
            f"{s['tag']} #seo #digitalmarketing")


def caption_li(s, idx=0):
    q = cta_bersih(s)
    pembuka = [s["li_par"], f"{s['li_par']} Pola ini berulang di banyak klien kami.",
               f"Dari pengalaman kami: {s['li_par'][0].lower()}{s['li_par'][1:]}"][idx % 3]
    return (f"{pembuka}\n\n{s['hook']}\n\n"
            f"Dua hal yang paling sering jadi penghambat:\n• {s['isi1']}\n• {s['isi2']}\n\n"
            f"Kerangka lengkapnya saya tulis di sini:\n{s['link']}\n\n"
            f"{q} Saya tertarik mendengar pengalaman teman-teman di kolom komentar.\n\n"
            f"{s['tag']} #seo #digitalmarketing")


def validasi(cap, link, maks):
    err = []
    if link not in cap:
        err.append("link hilang")
    if not re.search(r"komentar|diskusi|pendapat|tulis di|share|pengalaman|menurut", cap[-200:].lower()):
        err.append("tanpa ajakan diskusi di ekor")
    if "@" in cap:
        err.append("ada mention")
    bad = [t for t in re.findall(r"#(\w+)", cap) if t != t.lower()]
    if bad:
        err.append(f"hashtag kapital {bad}")
    if len(cap) > maks:
        err.append(f"kepanjangan {len(cap)}>{maks}")
    cjk = [ch for ch in cap if "\u3000" <= ch <= "\u9fff" or "\uac00" <= ch <= "\ud7af"]
    if cjk:
        err.append(f"CJK {cjk[:3]}")
    return err


def semua_jadwal(c, account_id):
    out, page = [], 1
    while True:
        r = c.get_schedules(page=page, limit=100)
        docs = r.get("docs") or r.get("data") or []
        if not docs:
            break
        out.extend([d for d in docs if d.get("accountId") == account_id])
        if len(docs) < 100:
            break
        page += 1
    return out


def slot_seri(c, account_id, jumlah):
    sekarang = datetime.now(WIB)
    terpakai = set()
    for d in semua_jadwal(c, account_id):
        if d.get("status") not in ("pending", "draft", "processing"):
            continue
        sa = d.get("scheduleAt") or ""
        if len(sa) >= 13:
            terpakai.add(sa[:13])
    hasil, hari = [], sekarang.date()
    for _ in range(60):
        if len(hasil) >= jumlah:
            break
        for jam in SLOTS_WIB:
            kandidat = datetime(hari.year, hari.month, hari.day, jam, 0, tzinfo=WIB)
            if kandidat <= sekarang + timedelta(minutes=20):
                continue
            kunci = kandidat.astimezone(timezone.utc).strftime("%Y-%m-%dT%H")
            if kunci in terpakai:
                continue
            terpakai.add(kunci)
            hasil.append(kandidat)
            break
        hari += timedelta(days=1)
    return hasil


def muat_state():
    return json.load(open(STATE)) if os.path.exists(STATE) else {"posted": []}


def simpan_state(s):
    json.dump(s, open(STATE, "w"), indent=1, ensure_ascii=False)


def sudah(state, key, nama):
    return any(p["akun"] == key and p["topik"] == nama for p in state["posted"])


def main():
    mode = sys.argv[1] if len(sys.argv) > 1 else "preview"
    jumlah = int(sys.argv[2]) if len(sys.argv) > 2 else len(S)
    state = muat_state()

    if mode == "preview":
        for t in TARGETS:
            maks = 500 if t["kind"] == "threads" else 3000
            build = {"threads": caption_threads, "fb": caption_fb, "li": caption_li}[t["kind"]]
            print(f"\n########## {t['label']} ##########")
            for i, s in enumerate(S[:jumlah]):
                cap = build(s, i)
                err = validasi(cap, s["link"], maks)
                print(f"--- [{i+1}] {s['nama']} | {len(cap)} char | {'OK' if not err else err}")
        return

    c = get_client()
    dibuat = 0
    for t in TARGETS:
        maks = 500 if t["kind"] == "threads" else 3000
        build = {"threads": caption_threads, "fb": caption_fb, "li": caption_li}[t["kind"]]
        sisa = [s for s in S if not sudah(state, t["key"], s["nama"])]
        print(f"\n=== {t['label']} — {len(sisa)} topik belum dijadwalkan ===")
        if not sisa:
            continue
        slots = slot_seri(c, t["account"], min(jumlah, len(sisa)))
        for s, slot in zip(sisa[:len(slots)], slots):
            cap = build(s, S.index(s))
            err = validasi(cap, s["link"], maks)
            if err:
                print(f"  ⚠️ SKIP {s['nama']}: {err}")
                continue
            payload = {
                "accountId": t["account"],
                "title": f"StudioSoft — {s['nama']}"[:90],
                "topic": "Seri SEO, Insight, Analitik & Data Marketing",
                "type": "text",
                "description": cap,
                "medias": [],
                "scheduleAt": slot.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z"),
                "replies": ([{"text": reply_threads(s), "username": ""}]
                            if t["kind"] == "threads" else []),
            }
            try:
                res = c.post("/public/schedule", payload)
                sid = res.get("scheduleId") or res.get("_id") or res.get("id")
                print(f"  ✅ {slot.strftime('%d %b %H:%M')} WIB | {s['nama'][:52]} | id={sid}")
                state["posted"].append({"akun": t["key"], "topik": s["nama"], "id": sid,
                                        "scheduleAt": payload["scheduleAt"], "link": s["link"]})
                simpan_state(state)
                dibuat += 1
            except ReplizError as e:
                print(f"  ❌ gagal {s['nama']}: {e}")
    print(f"\ntotal dibuat: {dibuat}")


if __name__ == "__main__":
    try:
        main()
    except ReplizError as e:
        print("ReplizError:", e)
        sys.exit(1)
