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

Topik Threads : "Iklan Facebook & Instagram untuk Rumah Sakit: Etis, Efektif & Sesuai Regulasi"
                (artikel https://studiosoft.id/iklan-facebook-rumah-sakit/ — BELUM pernah tayang
                 di Threads SS; hanya pernah jadi text-post pipeline artikel-share 9 Sep yang
                 GAGAL karena >500 char, tidak pernah diulang) -> carousel baru ss_hc39_iklanfb_*.
LinkedIn      : TEXT-only, artikel `klinik-kecil-butuh-digital-marketing` (FREE di antrean LI).
Facebook      : TEXT-only tanpa media, replies=[], artikel `panduan-seo-klinik-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 = "Iklan Facebook & Instagram untuk Rumah Sakit: Etis, Efektif & Sesuai Regulasi"
ARTICLE_URL = "https://studiosoft.id/iklan-facebook-rumah-sakit/"
LI_TITLE = "Mengapa Klinik Kecil Juga Butuh Digital Marketing \u2014 Mitos dan Fakta"
LI_URL = "https://studiosoft.id/klinik-kecil-butuh-digital-marketing/"
FB_TITLE = "Panduan SEO untuk Klinik dan Rumah Sakit: Biar Mudah Ditemukan Pasien di Google"
FB_URL = "https://studiosoft.id/panduan-seo-klinik-rumah-sakit/"
HASHTAG_LINE = "#studiosoft #iklanrs #digitalmarketing #rumahsakit #klinik"

MEDIA_FILES = [
    "ss_hc39_iklanfb_01_cover.jpg",
    "ss_hc39_iklanfb_02_tidakboleh.jpg",
    "ss_hc39_iklanfb_03_boleh.jpg",
    "ss_hc39_iklanfb_04_targeting.jpg",
    "ss_hc39_iklanfb_05_cta.jpg",
]
SRC_PNG = os.path.join(WS, "studiosoft_hc39_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 = """Iklan RS & klinik di Facebook/Instagram boleh jalan, asal tahu aturan mainnya.

Yang TIDAK boleh: klaim sembuh berlebihan, foto sebelum-sesudah tanpa disclaimer, testimoni tanpa izin pasien, promo obat resep.

Yang BOLEH: iklan edukasi, promosi bernarasi netral, brand awareness, retargeting.

Selengkapnya: """ + ARTICLE_URL

LI_BASE = """Dulu ada pemilik klinik kecil yang bilang: "Klinik saya kecil, cukup pasien datang sendiri." Setahun kemudian dia kembali dan bertanya kenapa kliniknya makin sepi.

Jawabannya bukan karena layanannya buruk. Tapi karena cara orang mencari layanan kesehatan sudah berubah. Saat butuh klinik gigi terdekat, orang tidak bertanya ke tetangga \u2014 mereka membuka Google Maps. Saat bingung soal ruam anak, mereka membaca review di grup Facebook sebelum memilih.

Empat mitos yang paling sering saya dengar dari pemilik klinik kecil:

Pertama, "digital marketing itu mahal". Faktanya bisa dimulai dari Rp0 \u2014 optimasi Google Business Profile gratis, konten edukasi gratis, SEO jangka panjang gratis. Bahkan iklan Facebook bisa mulai Rp20.000 per hari.

Kedua, "pasien tidak mencari klinik kecil di internet". Pasien tidak mencari "klinik besar" atau "klinik kecil" \u2014 mereka mencari solusi. Klinik kecil yang rajin SEO lokal bisa mengalahkan RS besar di Google Maps untuk radius tertentu.

Ketiga, "saya tidak punya waktu". Solusinya: serahkan ke tim yang mengerjakannya setiap hari \u2014 biasanya lebih murah daripada menggaji satu staf marketing tetap.

Keempat, "nanti saja kalau sudah ramai". Padahal urutannya terbalik: klinik jadi ramai karena lebih dulu ditemukan.

Langkah awalnya sederhana: optimasi Google Business Profile, buat Instagram dengan konten edukasi, pasang WhatsApp Business, mulai blog, lalu minta review dari pasien yang puas. Tidak perlu sekaligus \u2014 mulai dari yang gratis dulu.

Bacaan lengkapnya: """ + LI_URL

FB_BASE = """Bayangkan ada pasien yang baru pindah ke Surabaya dan butuh dokter kulit terdekat. Apa yang dia lakukan? Buka Google, ketik "klinik kulit Surabaya", lalu memilih satu dari sepuluh hasil pertama.

Di situlah SEO menentukan. RS atau klinik yang muncul di halaman pertama Google mendapat pasien baru setiap hari, selama 24 jam, tanpa bayar per klik.

Yang sering dilupakan: kata kunci di dunia kesehatan berbeda dengan bisnis biasa, karena niat pencarinya sangat spesifik.

Ada yang mencari dokter tertentu ("dokter spesialis jantung Surabaya"). Ada yang mencari informasi biaya ("biaya operasi katarak"). Ada yang siap datang sekarang ("klinik gigi terdekat dari lokasi saya"). Ada juga yang masih tahap edukasi, belum tentu booking.

Strategi SEO yang baik harus mencakup semuanya, dari yang informatif sampai yang transaksional.

Karena pasien mencari layanan di sekitar lokasinya, local SEO jadi prioritas nomor satu: Google Business Profile yang lengkap, NAP (nama, alamat, nomor telepon) yang konsisten di semua platform, pengelolaan review, pendaftaran di direktori kesehatan, dan konten yang menyebut lokasi.

Fondasi teknisnya juga tidak bisa dilewati: kecepatan website, struktur URL bersih, schema markup untuk dokter dan layanan, serta tampilan mobile yang nyaman.

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, 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", "rumah_sakit", ["netral"], limit=500, ban=BAN_RE,
                     curated=CURATED_T)
LI = build_safe(LI_BASE, "linkedin", "klinik", ["netral", "opini"], ban=BAN_RE, curated=CURATED_T)
FB = build_safe(FB_BASE, "facebook", "rumah_sakit", ["netral"], ban=BAN_RE, curated=CURATED_T)

REPLY1 = ("Baca selengkapnya: %s\n\nButuh strategi iklan RS & klinik yang etis dan terukur?\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_hc39_iklanfb_")):
            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 hc39 sudah pernah dijadwalkan?):")
    hits = scan_media(("pending", "scheduled", "draft", "success"))
    if hits:
        print("  \u26a0\ufe0f sudah ada jadwal dgn media hc39 -> 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", 34) + 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] hc39 (topik BELUM tayang di Threads: iklan FB/IG utk RS) | %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 17 Sep, terbaru 17 Sep = non-healthcare); artikel iklan-facebook-rumah-sakit "
                    "hanya pernah jadi text-post GAGAL 9 Sep (>500 char) -> carousel BARU nama file "
                    "ss_hc39_iklanfb_* + engine ss_carousel_lib (bg #122B63); LinkedIn pakai artikel "
                    "klinik-kecil-butuh-digital-marketing (FREE di antrean LI); FB pakai artikel panduan SEO "
                    "klinik/RS (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)
