#!/usr/bin/env python3
"""StudioSoft Healthcare — run cron hc40 (19 Sep 2026 02:00 WIB / 18 Sep 19:00 UTC).

Topik Threads : "Mengapa Klinik Kecil Juga Butuh Digital Marketing \u2014 Mitos dan Fakta"
                (artikel https://studiosoft.id/klinik-kecil-butuh-digital-marketing/ —
                 BELUM pernah tayang di Threads SS; hanya pernah jadi text-post pipeline
                 artikel-share 9 Sep yang GAGAL >500 char, tidak pernah diulang)
                 -> carousel baru ss_hc40_klinikkecil_*.
LinkedIn      : TEXT-only, artikel `9-contoh-konten-digital-marketing-rumah-sakit-yang-
                edukatif-dan-tetap-etis` (di antrean LI hanya pernah status error 5 Sep
                -> belum pernah tayang di LinkedIn SS, tidak ada di queue pending).
Facebook      : TEXT-only tanpa media, replies=[], artikel `iklan-facebook-rumah-sakit`
                (belum pernah dipakai di FB SS).

ATURAN (FIXED 5 & 15 Sep 2026):
- HANYA THREADS SS yang boleh carousel/image. FB & LinkedIn SS: TEXT saja.
- IG SS tidak disentuh (punya desain sendiri).
- Carousel = 09:00 WIB. Threads <=500 char, hashtag huruf kecil, 0 mention, ada pancingan diskusi.
- Standar desain 16 Sep 2026: engine ss_carousel_lib.py, bg #122B63, rata kiri, blok tengah
  vertikal, tanpa separator, bullet dot, Poppins Bold 85 / Regular 36 / sub 30, auto-fit.
"""
import json, os, re, subprocess, sys
from datetime import datetime, timezone, timedelta

WS = "/home/mantapman/.hermes/profiles/atun/workspace"
sys.path.insert(0, WS)
from PIL import Image
from repliz_client import ReplizClient, ACC, IMG_DIR, IMG_BASE_URL
from diskusi_engine import tambahkan_diskusi

DRY = os.environ.get("DRY", "") == "1"
c = ReplizClient()

TITLE = "Mengapa Klinik Kecil Juga Butuh Digital Marketing \u2014 Mitos dan Fakta"
ARTICLE_URL = "https://studiosoft.id/klinik-kecil-butuh-digital-marketing/"
LI_TITLE = "9 Contoh Konten Digital Marketing Rumah Sakit yang Edukatif dan Tetap Etis"
LI_URL = "https://studiosoft.id/9-contoh-konten-digital-marketing-rumah-sakit-yang-edukatif-dan-tetap-etis/"
FB_TITLE = "Iklan Facebook & Instagram untuk Rumah Sakit: Etis, Efektif, dan Sesuai Regulasi"
FB_URL = "https://studiosoft.id/iklan-facebook-rumah-sakit/"
HASHTAG_LINE = "#studiosoft #klinikkecil #digitalmarketing #klinik #kesehatan"

MEDIA_FILES = [
    "ss_hc40_klinikkecil_01_cover.jpg",
    "ss_hc40_klinikkecil_02_mitos12.jpg",
    "ss_hc40_klinikkecil_03_mitos34.jpg",
    "ss_hc40_klinikkecil_04_langkah.jpg",
    "ss_hc40_klinikkecil_05_cta.jpg",
]
SRC_PNG = os.path.join(WS, "studiosoft_hc40_carousel")
MEDIA_URLS = [IMG_BASE_URL.rstrip("/") + "/openclaw-img/" + f for f in MEDIA_FILES]
INGRESS = ["103.84.155.153", "103.84.155.217"]

THREADS_BASE = """Banyak pemilik klinik kecil berpikir: "Digital marketing itu buat rumah sakit besar."

Faktanya justru sebaliknya \u2014 klinik kecil paling diuntungkan: biaya lebih rendah, jangkauan lebih tepat, hasil lebih cepat terasa.

Empat mitos vs faktanya:
""" + ARTICLE_URL

LI_BASE = """Dulu saya mengira promosi rumah sakit cuma soal seberapa rajin muncul di media sosial. Sampai seorang manajer RS bercerita: kontennya tayang tiap hari, tapi calon pasien justru makin bingung mau berbuat apa.

Setelah kami bedah bareng, masalahnya bukan di frekuensi \u2014 tapi di jenis kontennya. Ada sembilan jenis konten yang terbukti edukatif sekaligus aman secara etika:

Pertama, edukasi gejala dan pencegahan penyakit. Bahasa sederhana, fokus pada apa yang bisa dilakukan pasien, bukan sekadar memamerkan fasilitas.

Kedua, sesi "Tanya Dokter" format Q&A. Sangat disukai karena menjawab pertanyaan nyata, asal tidak mendiagnosis spesifik dan tetap mengarahkan konsultasi langsung.

Ketiga, tips gaya hidup sehat. Aman, bermanfaat, dan membangun citra RS yang peduli pencegahan, bukan cuma pengobatan.

Keempat, edukasi prosedur medis dengan bahasa awam \u2014 fokus pada proses, keamanan, dan pendampingan pasien.

Kelima, profil dokter dan tenaga medis yang humanis: minat, alasan memilih profesi, pesan untuk pasien. Bukan klaim "paling hebat".

Keenam, testimoni yang disaring: wajib ada izin, identitas dilindungi bila perlu, dan fokus pada pengalaman pelayanan.

Ketujuh, kampanye kesehatan dan program sosial seperti bakti kesehatan atau donor darah.

Kedelapan, FAQ seputar layanan \u2014 cara daftar online, syarat rawat inap, metode pembayaran.

Kesembilan, pengingat pemeriksaan berkala: imunisasi anak, kontrol tekanan darah, medical check-up.

Kesimpulannya: konten kesehatan yang baik selalu menempatkan keselamatan dan pemahaman pasien di depan, bukan janji berlebihan. Dari situ kepercayaan tumbuh, dan pasien datang dengan sendirinya.

Bacaan lengkapnya: """ + LI_URL

FB_BASE = """Pertanyaan yang paling sering saya dengar dari manajemen RS dan klinik: "Boleh tidak sih layanan kesehatan diiklankan di Facebook dan Instagram?"

Jawabannya: boleh, dan justru sangat bisa efektif \u2014 asal tahu aturan mainnya. Ada etika kedokteran dan kebijakan Meta yang harus dihormati.

Yang TIDAK boleh, antara lain: klaim kesembuhan berlebihan seperti "sembuh total dalam 1 hari", foto sebelum-sesudah sensasional tanpa disclaimer, testimoni tanpa izin pasien, promosi obat resep langsung ke konsumen, dan konten yang menakut-nakuti (fear-based marketing).

Yang BOLEH dan biasanya paling berhasil: iklan edukasi, promosi layanan bernarasi netral (misal "kami buka layanan konsultasi kulit setiap Sabtu"), brand awareness institusi, serta retargeting untuk orang yang sudah membuka website tapi belum booking.

Kekuatan Facebook & Instagram Ads ada di ketepatan sasaran. Iklan layanan kandungan bisa menyasar wanita 24-40 tahun di radius 15 km dari RS. Iklan sunat metode modern menyasar orang tua dengan anak usia 5-12 tahun. Iklan medical check-up menyasar usia 35-60 tahun dengan minat kesehatan.

Soal budget, tidak perlu langsung jutaan. Mulai Rp50.000-100.000 per hari untuk menguji kreatif mana yang paling efektif, lalu scale up yang sudah terbukti. Dengan data analytics, setiap rupiah bisa dipertanggungjawabkan.

Bacaan lengkapnya: """ + FB_URL

CTA_RE = re.compile(r"komentar|pendapat|diskusi|setuju|share|pengalaman|menurut|bagaimana|apa yang", re.I)
BAN_RE = re.compile(r"kecantikan|skincare|jerawat|sembuh total", re.I)
CURATED_T = [
    "Menurut Anda, mitos mana yang paling sering Anda dengar soal klinik kecil? Yuk diskusi di komentar.",
    "Bold statement: bukan RS besar yang paling butuh digital marketing, justru klinik kecil. Setuju atau tidak?",
    "Buat yang kanal digital kliniknya sudah rapi, Anda selangkah lebih maju! \U0001f44f",
]
CURATED_LI = [
    "Menurut Anda, jenis konten mana yang paling sering dilupakan rumah sakit? Yuk diskusi di komentar.",
    "Bold statement: konten kesehatan yang etis justru lebih dipercaya pasien. Setuju atau tidak?",
    "Buat tim RS yang konsisten mengedukasi pasien, Anda selangkah lebih maju! \U0001f44f",
]
CURATED_FB = [
    "Menurut Anda, iklan atau edukasi yang lebih dulu harus jalan? Yuk diskusi di komentar.",
    "Bold statement: RS boleh beriklan, asal tidak menjual janji kesembuhan. Setuju atau tidak?",
    "Buat yang iklannya sudah etis dan terukur, Anda selangkah lebih maju! \U0001f44f",
]


def build(base, platform, industri, tones, limit=None, ban=None, max_try=120):
    for tone in tones:
        for _ in range(max_try):
            txt = tambahkan_diskusi(base, platform=platform, industri=industri, tone=tone, force=True)
            if not CTA_RE.search(txt[-200:]):
                continue
            if limit is not None and len(txt) > limit:
                continue
            if ban is not None and ban.search(txt):
                continue
            if "..." in txt:
                continue
            return txt
    return None


def build_safe(base, platform, industri, tones, limit=None, ban=None, curated=None):
    txt = build(base, platform, industri, tones, limit=limit, ban=ban)
    if txt is not None:
        return txt
    for hook in sorted(curated or [], key=len):
        cand = base.rstrip() + "\n\n" + hook
        if CTA_RE.search(cand[-200:]) and (limit is None or len(cand) <= limit):
            return cand
    return None


THREADS = build_safe(THREADS_BASE, "threads", "klinik", ["netral"], limit=500, ban=BAN_RE,
                     curated=CURATED_T)
LI = build_safe(LI_BASE, "linkedin", "klinik", ["netral", "opini"], ban=BAN_RE, curated=CURATED_LI)
FB = build_safe(FB_BASE, "facebook", "rumah_sakit", ["netral"], ban=BAN_RE, curated=CURATED_FB)

REPLY1 = ("Baca selengkapnya: %s\n\nButuh bantuan mengelola kanal digital klinik Anda?\n"
          "www.studiosoft.id" % ARTICLE_URL)
REPLY2 = HASHTAG_LINE


def check_caption(name, txt, limit=None, require_cta=True):
    ok = True
    if txt is None:
        print("  \u274c %s: gagal disusun" % name)
        return False
    if limit and len(txt) > limit:
        print("  \u274c %s: %d char > %d" % (name, len(txt), limit))
        ok = False
    m = re.findall(r"@\w+", txt)
    if m:
        print("  \u274c %s: ada mention %s" % (name, m))
        ok = False
    caps = [t for t in re.findall(r"#(\w+)", txt) if t != t.lower()]
    if caps:
        print("  \u274c %s: hashtag KAPITAL %s" % (name, caps))
        ok = False
    if require_cta and not CTA_RE.search(txt[-200:]):
        print("  \u274c %s: tidak ada pancingan diskusi di ekor caption" % name)
        ok = False
    print("  %s %s: %d char" % ("\u2705" if ok else "\u26a0\ufe0f", name, len(txt)))
    return ok


def all_docs():
    docs, page = [], 1
    while True:
        d = c.get("/public/schedule?page=%d&limit=100" % page)
        docs.extend(d.get("docs", []))
        if page >= (d.get("totalPages") or 1):
            break
        page += 1
    return docs


def scan_media(statuses=("pending", "scheduled", "draft")):
    hits = []
    for s in all_docs():
        if s.get("accountId") not in (ACC.STUDIOSOFT_THREADS, ACC.STUDIOSOFT_LINKEDIN,
                                      ACC.STUDIOSOFT_FB, ACC.STUDIOSOFT_IG):
            continue
        if (s.get("status") or "").lower() not in statuses:
            continue
        for m in (s.get("medias") or []):
            base = os.path.basename((m.get("url") or "").split("?")[0]).lower()
            if base in [f.lower() for f in MEDIA_FILES]:
                hits.append((s.get("accountId"), (s.get("scheduleAt") or "")[:16],
                             (s.get("title") or "")[:40], s.get("type"), s.get("status")))
                break
    return hits


def busy_by_account(aid):
    busy = set()
    for s in all_docs():
        if s.get("accountId") == aid and (s.get("status") or "").lower() in ("pending", "scheduled", "draft"):
            busy.add((s.get("scheduleAt") or "")[:19])
    return busy


def next_free_09():
    busy = busy_by_account(ACC.STUDIOSOFT_THREADS)
    now = datetime.now(timezone.utc)
    start = datetime(now.year, now.month, now.day, tzinfo=timezone.utc)
    if now.hour >= 2:
        start += timedelta(days=1)
    for day in range(0, 20):
        cand = (start + timedelta(days=day)).strftime("%Y-%m-%dT02:00:00")
        if cand not in busy:
            lab = (datetime.fromisoformat(cand) + timedelta(hours=7)).strftime("%d %b %Y 09:00 WIB")
            return cand + ".000Z", lab
    return None, None


def next_free_li():
    SLOTS = [("05:00 WIB", 22, -1), ("08:00 WIB", 1, 0), ("11:00 WIB", 4, 0), ("14:00 WIB", 7, 0),
             ("17:00 WIB", 10, 0), ("20:00 WIB", 13, 0), ("23:00 WIB", 16, 0)]
    busy = busy_by_account(ACC.STUDIOSOFT_LINKEDIN)
    now = datetime.now(timezone.utc) + timedelta(minutes=15)
    now_naive = now.replace(tzinfo=None)
    today = datetime.now(timezone.utc)
    for day in range(0, 60):
        for label, uh, shift in SLOTS:
            d0 = datetime(today.year, today.month, today.day, tzinfo=timezone.utc) + timedelta(days=day + shift)
            cand = "%sT%02d:00:00" % (d0.strftime("%Y-%m-%d"), uh)
            if datetime.fromisoformat(cand) <= now_naive:
                continue
            if cand not in busy:
                lab = (datetime.fromisoformat(cand) + timedelta(hours=7)).strftime("%d %b %Y %H:%M WIB")
                return cand + ".000Z", lab
    return None, None


def next_free_fb():
    busy = busy_by_account(ACC.STUDIOSOFT_FB)
    now = datetime.now(timezone.utc) + timedelta(minutes=30)
    now_naive = now.replace(tzinfo=None)
    today = datetime.now(timezone.utc)
    for day in range(0, 21):
        d0 = datetime(today.year, today.month, today.day, tzinfo=timezone.utc) + timedelta(days=day)
        cand = "%sT12:30:00" % d0.strftime("%Y-%m-%d")
        if datetime.fromisoformat(cand) <= now_naive:
            continue
        if cand not in busy:
            lab = (datetime.fromisoformat(cand) + timedelta(hours=7)).strftime("%d %b %Y 19:30 WIB")
            return cand + ".000Z", lab
    return None, None


def prep_media():
    ok = True
    for png in sorted(os.listdir(SRC_PNG)):
        if not (png.endswith(".png") and png.startswith("ss_hc40_klinikkecil_")):
            continue
        jpg = png.replace(".png", ".jpg")
        src = Image.open(os.path.join(SRC_PNG, png))
        if src.mode != "RGB":
            src = src.convert("RGB")
        src.save(os.path.join(IMG_DIR, jpg), "JPEG", quality=90)
        print("  \u2705 %s -> %s %s" % (png, jpg, src.size))
    for f in MEDIA_FILES:
        try:
            im = Image.open(os.path.join(IMG_DIR, f))
            im.verify()
            im = Image.open(os.path.join(IMG_DIR, f))
            print("  \u2705 PIL %s %s %s %d bytes" % (f, im.size, im.mode,
                                                      os.path.getsize(os.path.join(IMG_DIR, f))))
        except Exception as e:
            print("  \u274c PIL %s: %s" % (f, e))
            ok = False
    return ok


def verify_urls():
    ok = True
    for u in MEDIA_URLS:
        for ip in INGRESS:
            r = subprocess.run(["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "--max-time", "30",
                                "--resolve", "mantapserver.tail3947bb.ts.net:443:%s" % ip, u],
                               capture_output=True, text=True)
            code = r.stdout.strip()
            print("  %s @%s: %s" % (u.split("/")[-1], ip, code))
            if code != "200":
                ok = False
    return ok


def verify_link(url):
    r = subprocess.run(["curl", "-sIL", "--http1.1", "--max-time", "200",
                        "-A", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", url,
                        "-o", "/dev/null", "-w", "%{http_code}"], capture_output=True, text=True)
    code = (r.stdout or "").strip()
    print("  langsung %s: %s" % (url, code))
    if code != "200":
        r2 = subprocess.run(["curl", "-s", "--max-time", "120", "-o", "/dev/null", "-w", "%{http_code}",
                             "https://r.jina.ai/" + url], capture_output=True, text=True)
        code = (r2.stdout or "").strip()
        print("  via r.jina.ai: %s" % code)
    return code == "200"


def main():
    wib = datetime.now(timezone(timedelta(hours=7)))
    print("Time WIB: %s" % wib.strftime("%Y-%m-%d %H:%M"))

    print("\n1) SLOT:")
    SLOT_UTC, SLOT_LABEL = next_free_09()
    if not SLOT_UTC:
        print("  \u274c tidak ada slot 09:00 WIB bebas \u2014 batal.")
        return False
    print("  \u2705 carousel Threads: %s (%s)" % (SLOT_LABEL, SLOT_UTC))
    LI_SLOT_UTC, LI_SLOT_LABEL = next_free_li()
    FB_SLOT_UTC, FB_SLOT_LABEL = next_free_fb()
    print("  \u2705 LinkedIn text: %s (%s)" % (LI_SLOT_LABEL, LI_SLOT_UTC))
    print("  \u2705 Facebook text (tanpa media, replies=[]): %s (%s)" % (FB_SLOT_LABEL, FB_SLOT_UTC))
    if not (LI_SLOT_UTC and FB_SLOT_UTC):
        print("  \u274c slot LI/FB tidak ditemukan \u2014 batal.")
        return False

    print("\n2) CAPTION:")
    ok = True
    ok &= check_caption("Threads", THREADS, 500)
    ok &= check_caption("LinkedIn", LI)
    ok &= check_caption("Facebook", FB)
    ok &= check_caption("Reply1", REPLY1, require_cta=False)
    ok &= check_caption("Reply2", REPLY2, require_cta=False)
    if not ok:
        print("\n\u274c caption gagal aturan \u2014 batal.")
        return False

    print("\n3) IDEMPOTENSI (media hc40 sudah pernah dijadwalkan?):")
    hits = scan_media(("pending", "scheduled", "draft", "success"))
    if hits:
        print("  \u26a0\ufe0f sudah ada jadwal dgn media hc40 -> SKIP create:")
        for h in hits:
            print("   ", h)
        return True
    print("  \u2705 belum ada")

    print("\n4) MEDIA: PNG standar baru -> JPEG openclaw-img + PIL:")
    if not prep_media():
        print("  \u274c media rusak \u2014 batal.")
        return False

    print("\n5) MEDIA JALUR INTERNET (2 ingress):")
    if not verify_urls():
        print("  \u274c ada media tidak 200 \u2014 batal.")
        return False

    print("\n6) LINK ARTIKEL:")
    for u in (ARTICLE_URL, LI_URL, FB_URL):
        if not verify_link(u):
            print("  \u274c link tidak 200 \u2014 batal.")
            return False

    if DRY:
        print("\n[DRY RUN] semua cek lolos; tidak ada create.")
        return True

    base_dt = datetime.fromisoformat(SLOT_UTC.replace("Z", "+00:00"))
    r2 = (base_dt + timedelta(minutes=1)).strftime("%Y-%m-%dT%H:%M:%S.000Z")
    r3 = (base_dt + timedelta(minutes=2)).strftime("%Y-%m-%dT%H:%M:%S.000Z")
    medias = [c.make_media(u, "image", "slide%d" % (i + 1)) for i, u in enumerate(MEDIA_URLS)]

    results = {}
    print("\n7) CREATE \u2014 THREADS (album + 2 replies):")
    try:
        alb = c.schedule_album(ACC.STUDIOSOFT_THREADS, THREADS, SLOT_UTC, medias, title=TITLE)
        pid = alb.get("scheduleId") or alb.get("_id") or alb.get("id")
        print("  album -> %s" % pid)
        c.create_schedule(ACC.STUDIOSOFT_THREADS, REPLY1, r2, type_="text", replies=[pid])
        c.create_schedule(ACC.STUDIOSOFT_THREADS, REPLY2, r3, type_="text", replies=[pid])
        print("  2 replies OK")
        results["threads"] = pid
    except Exception as e:
        print("  GAGAL %s" % e)
        results["threads"] = None

    print("\n   LINKEDIN (text-only, %s):" % LI_SLOT_LABEL)
    try:
        res = c.create_schedule(ACC.STUDIOSOFT_LINKEDIN, LI, LI_SLOT_UTC, title=LI_TITLE,
                                type_="text", medias=[], replies=[])
        print("  text -> %s" % (res.get("scheduleId") or res.get("_id") or res.get("id")))
        results["linkedin"] = res.get("scheduleId") or res.get("_id") or res.get("id")
    except Exception as e:
        print("  GAGAL %s" % e)
        results["linkedin"] = None

    print("\n   FACEBOOK (TEXT saja, TANPA media, replies=[], %s):" % FB_SLOT_LABEL)
    try:
        res = c.create_schedule(ACC.STUDIOSOFT_FB, FB, FB_SLOT_UTC, title=FB_TITLE,
                                type_="text", medias=[], replies=[])
        print("  text -> %s" % (res.get("scheduleId") or res.get("_id") or res.get("id")))
        results["facebook"] = res.get("scheduleId") or res.get("_id") or res.get("id")
    except Exception as e:
        print("  GAGAL %s" % e)
        results["facebook"] = None

    print("\n" + "=" * 55)
    ok = all(results.values())
    print("SEMUA PLATFORM TERJADWAL" if ok else "GAGAL: %s" % [k for k, v in results.items() if not v])

    if ok:
        print("\n8) VERIFIKASI (fetch ULANG dari API per jadwal):")
        for name, sid in results.items():
            d = c.get("/public/schedule/%s" % sid)
            doc = d.get("doc") or d.get("data") or d
            print("  %s: status=%s at=%s type=%s medias=%d replies=%d" % (
                name, doc.get("status"), doc.get("scheduleAt"), doc.get("type"),
                len(doc.get("medias") or []), len(doc.get("replies") or [])))
        for h in scan_media(("pending",)):
            print("   ", h)

        st = json.loads(open(WS + "/studiosoft_healthcare_state.json").read())
        st["index"] = st.get("index", 35) + 1
        st["topic"] = TITLE
        st["last_post"] = wib.strftime("%Y-%m-%d %H:%M")
        json.dump(st, open(WS + "/studiosoft_healthcare_state.json", "w"))
        with open(WS + "/studiosoft_healthcare_log.txt", "a") as f:
            f.write("\n[%s WIB] hc40 (topik BELUM tayang di Threads: klinik kecil butuh digital "
                    "marketing) | %s | %s | Threads album %s (%s) + 2 replies | LinkedIn TEXT "
                    "\"%s\" %s (%s) | FB TEXT \"%s\" (tanpa media, replies=[]) %s | catatan: stok "
                    "artikel healthcare baru masih HABIS (WP REST 18 Sep, terbaru 18 Sep = "
                    "non-healthcare); artikel klinik-kecil-butuh-digital-marketing hanya pernah "
                    "jadi text-post GAGAL 9 Sep (>500 char) -> carousel BARU nama file "
                    "ss_hc40_klinikkecil_* + engine ss_carousel_lib (bg #122B63); LinkedIn pakai "
                    "artikel 9-contoh-konten (di antrean LI hanya pernah ERROR 5 Sep); FB pakai "
                    "artikel iklan-facebook-rumah-sakit (BELUM pernah di FB SS); state index -> %s.\n"
                    % (wib.strftime("%Y-%m-%d %H:%M"), TITLE, ARTICLE_URL, SLOT_LABEL,
                       SLOT_UTC[11:16] + "Z", LI_TITLE, LI_SLOT_LABEL, LI_SLOT_UTC,
                       FB_TITLE, FB_SLOT_LABEL, st["index"]))
        print("Log + state ditulis.")
    return ok


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