#!/usr/bin/env python3
"""StudioSoft Healthcare — run cron hc38 (RE-AIR hc21/hc35 ke THREADS, slide BARU standar 16 Sep).

Cron run 17 Sep 2026 (02:00 WIB / 16 Sep 19:00 UTC).

Konteks:
- Stok artikel healthcare BARU masih HABIS (WP REST 16 Sep: artikel terbaru 13 Sep; satu-satunya
  artikel healthcare di batch itu = jasa-digital-marketing-klinik, sudah dipakai hc36).
- Topik "5 Kesalahan Fatal Website RS & Klinik" (hc21/hc35) BELUM PERNAH tayang di Threads:
  2x error "The media could not be fetched from this URI" (26 Agu & 13 Sep) pada file ss_hc21_*.
  Di FB album 13 Sep justru SUKSES -> konten valid, masalahnya jalur fetch media.
  Mitigasi: slide dibuat ULANG dgn NAMA FILE BARU ss_hc38_website_* (pola hc37 yg berhasil).
- Slide memakai ENGINE BARU ss_carousel_lib.py (standar desain Bos 16 Sep 2026:
  #122B63, rata kiri, blok tengah vertikal, tanpa separator, bullet dot, Poppins 85/36/30, auto-fit).

ATURAN (FIXED 5 & 15 Sep 2026 - instruksi Bos):
- HANYA THREADS StudioSoft yang boleh carousel/image.
- Facebook StudioSoft: TIDAK ada gambar/carousel. Diberi TEXT dari artikel, replies=[] (FB tidak
  support replies -> error "Cannot read properties of undefined (reading 'postId')").
- LinkedIn StudioSoft: TEXT-only dari artikel (tanpa image), cerita + ajakan diskusi, replies=[].
- Instagram StudioSoft: tidak disentuh (punya desain sendiri, di luar scope).
- Carousel = 09:00 WIB. Threads <=500 char. Hashtag huruf kecil. 0 mention. Wajib pancingan diskusi.
"""
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 = "5 Kesalahan Fatal Website RS & Klinik yang Bikin Pasien Kabur"
ARTICLE_URL = "https://studiosoft.id/website-rumah-sakit-gagal-pasien/"
LI_TITLE = "Google Business Profile untuk Klinik: Cara Biar Pasien Gampang Nemu Lokasi Anda"
LI_URL = "https://studiosoft.id/google-business-profile-klinik/"
FB_TITLE = "Social Media Marketing untuk Rumah Sakit: Platform Mana yang Paling Efektif?"
FB_URL = "https://studiosoft.id/social-media-rumah-sakit-efektif/"
HASHTAG_LINE = "#studiosoft #websiters #digitalmarketing #rumahsakit #klinik"

MEDIA_FILES = [
    "ss_hc38_website_01_cover.jpg",
    "ss_hc38_website_02_kesalahan12.jpg",
    "ss_hc38_website_03_kesalahan34.jpg",
    "ss_hc38_website_04_solusi.jpg",
    "ss_hc38_website_05_cta.jpg",
]
SRC_PNG = os.path.join(WS, "studiosoft_hc38_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 = """Website RS yang lambat dan membingungkan bukan sekadar tidak rapi \u2014 ia mengantar pasien ke RS lain.

5 kesalahan fatal:
\u2022 Loading >3 detik: 53% pengunjung langsung pergi
\u2022 Info dokter & jadwal praktik sulit ditemukan
\u2022 Tidak mobile-friendly (70% pasien cari dari HP)
\u2022 CTA lemah: tanpa tombol booking/WhatsApp
\u2022 Tanpa integrasi sistem booking online

Selengkapnya: """ + ARTICLE_URL

LI_BASE = """Dulu saya pikir cara paling cepat mendatangkan pasien adalah menambah anggaran iklan. Setelah membedah puluhan website RS dan klinik, saya berubah pikiran: ada aset digital gratis yang paling sering diabaikan, yaitu Google Business Profile.

Efeknya langsung terasa. Pasien yang mencari "klinik gigi terdekat" akan melihat profil klinik Anda di peta \u2014 lengkap dengan rating, jam buka, dan tombol Petunjuk Arah dalam satu klik. Kalau profilnya kosong atau datanya tidak akurat, pasien akan memilih klinik lain yang tampil lebih meyakinkan.

Langkah optimasinya tidak rumit: verifikasi kepemilikan, lengkapi data (alamat, jam operasional termasuk weekend dan hari libur, nomor telepon, website), pilih kategori yang tepat, unggah minimal 20 foto \u2014 eksterior, interior, ruang tunggu, dokter, peralatan, dan tim \u2014 lalu posting rutin.

Bagian yang paling sering diabaikan justru review. Klinik dengan rating 4,5 ke atas hampir selalu lebih dipilih daripada yang 3,5, meski layanannya bisa jadi setara. Minta review dari pasien yang puas lewat QR code di meja resepsionis, tanggapi semua review maksimal 24 jam, dan jangan pernah membeli review palsu.

Studi kasus klien kami: klinik gigi di Surabaya yang merapikan GBP \u2014 data lengkap, foto baru, dan posting rutin \u2014 mengalami kenaikan permintaan Petunjuk Arah sampai 40% dalam tiga bulan. Jumlah review naik dari 12 menjadi 87, rating terjaga di 4,6.

Pelajaran yang saya ambil: sebelum menambah anggaran iklan, pastikan dulu aset gratis kita sudah bekerja maksimal.

Bacaan lengkapnya: """ + LI_URL

FB_BASE = """Kalau RS punya akun di semua platform tapi kontennya itu-itu saja, masalahnya bukan di platformnya \u2014 tapi di strategi kontennya.

Setiap platform punya karakter sendiri:
\u2022 Instagram \u2014 carousel edukasi, reels penjelasan dokter, story Q&A. Komposisi konten: 70% edukasi, 20% promosi, 10% behind the scene.
\u2022 Facebook \u2014 masih kuat untuk usia 35+, yang justru segmen pasien utama RS. Cocok untuk artikel panjang, event, dan grup diskusi kesehatan.
\u2022 YouTube \u2014 paling underutilized. Video edukasi dokter bisa muncul di pencarian Google sekaligus YouTube.
\u2022 TikTok \u2014 edukasi ringan, mitos vs fakta, dan tips cepat untuk audiens muda.
\u2022 LinkedIn \u2014 branding institusi, publikasi riset, dan rekrutmen tenaga medis.

Kuncinya: tidak perlu aktif di semua platform sekaligus. Pilih 2-3 yang paling relevan dengan target pasien Anda, lalu konsisten.

Bacaan lengkapnya: """ + FB_URL

CTA_RE = re.compile(r"komentar|pendapat|diskusi|setuju|share|pengalaman|menurut", re.I)
BAN_RE = re.compile(r"kecantikan|skincare|kulit|jerawat", re.I)
CURATED_T = [
    "Menurut Anda, apa yang paling perlu dibenahi dulu? Yuk diskusi di komentar.",
    "Bold statement: website lambat = pasien hilang. Setuju atau tidak?",
    "Buat yang websitenya sudah cepat & mobile-friendly, Anda selangkah lebih maju! \U0001f44f",
]


def build(base, platform, industri, tones, limit=None, ban=None, max_try=80):
    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) 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", "rumah_sakit", ["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 audit website RS & klinik Anda?\nwww.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")):
    """FETCH ULANG dari API setiap dipanggil (jangan pakai snapshot lama)."""
    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():
    """Slot 09:00 WIB (02:00Z) bebas pertama utk Threads SS, dihitung DARI API."""
    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():
    """7 slot WIB LI kosong pertama (dihitung dari API). Slot yang sudah lewat DILEWATI."""
    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():
    """Slot 19:30 WIB (12:30Z) bebas pertama utk FB SS."""
    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, 14):
        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():
    """PNG (standar baru) -> JPEG di openclaw-img + verifikasi 200 di KEDUA ingress."""
    ok = True
    for png, jpg in zip(sorted(os.listdir(SRC_PNG)), MEDIA_FILES):
        if not png.endswith(".png"):
            continue
        if not png.startswith("ss_hc38_website_"):
            continue
        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 hc38 sudah pernah dijadwalkan?):")
    hits = scan_media(("pending", "scheduled", "draft", "success"))
    if hits:
        print("  \u26a0\ufe0f sudah ada jadwal dgn media hc38 -> 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", 33) + 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] hc38 (RE-AIR hc21/hc35 ke Threads, slide BARU standar 16 Sep) | %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 16 Sep, terbaru 13 Sep); topik website-RS belum pernah tayang di Threads "
                    "(2x error media-fetch pada ss_hc21_*) -> slide dibuat ULANG dgn nama file baru "
                    "ss_hc38_website_* + engine ss_carousel_lib (bg #122B63); LinkedIn pakai artikel "
                    "panduan GBP (belum ada di antrean LI) & FB pakai artikel social media RS; "
                    "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)
