#!/usr/bin/env python3
"""StudioSoft Healthcare — hc36 "Jasa Digital Marketing Klinik: Cara Menarik Pasien Baru dari Google".

Cron run 14 Sep 2026 (02:00 WIB):
- Artikel BARU studiosoft.id dipublikasikan 13 Sep 2026 07:42 WIB (setelah cron run sebelumnya):
  https://studiosoft.id/jasa-digital-marketing-klinik/ — healthcare (klinik), belum pernah dipakai
  di jadwal mana pun (diverifikasi: 0 hit di 3.909 jadwal Repliz).
- Carousel 5 slide dibuat dari isi artikel (studio hc36, navy #0D4761 + sky #38BDF8).
- Aturan: carousel = 09:00 WIB; Threads + FB album; Instagram TIDAK disentuh;
  LinkedIn TIDAK diberi image/carousel -> LinkedIn = TEXT bahas artikel + ajakan diskusi.
- FB: album replies KOSONG. Threads: album + 2 replies (reply1 = link, reply2 = hashtag).
- Hashtag huruf kecil, 0 mention, caption wajib pancingan diskusi, Threads <=500 char.
"""
import json, os, re, subprocess, sys
from datetime import datetime, timezone, timedelta

sys.path.insert(0, "/home/mantapman/.openclaw/workspace-atun")
from repliz_client import ReplizClient, ACC
from diskusi_engine import tambahkan_diskusi

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

SLOT_UTC = "2026-09-14T02:00:00.000Z"        # 14 Sep 2026 09:00 WIB (carousel = jam 9)
SLOT_LABEL = "14 Sep 2026 09:00 WIB"
LI_SLOT_UTC = "2026-10-09T22:00:00.000Z"     # 10 Okt 2026 05:00 WIB (slot LI kosong pertama)
LI_SLOT_LABEL = "10 Okt 2026 05:00 WIB"

TITLE = "Jasa Digital Marketing Klinik: Cara Menarik Pasien Baru dari Google"
ARTICLE_URL = "https://studiosoft.id/jasa-digital-marketing-klinik/"
HASHTAG_LINE = "#studiosoft #marketingklinik #digitalmarketing #klinik #rumahsakit"
LI_HASHTAG = "#studiosoft #marketingklinik #digitalmarketing #klinik #layanankesehatan"

MEDIA_URLS = [
    "https://mantapserver.tail3947bb.ts.net/openclaw-img/ss_hc36_klinikdm_01_cover.jpg",
    "https://mantapserver.tail3947bb.ts.net/openclaw-img/ss_hc36_klinikdm_02_perjalanan.jpg",
    "https://mantapserver.tail3947bb.ts.net/openclaw-img/ss_hc36_klinikdm_03_strategi13.jpg",
    "https://mantapserver.tail3947bb.ts.net/openclaw-img/ss_hc36_klinikdm_04_strategi45.jpg",
    "https://mantapserver.tail3947bb.ts.net/openclaw-img/ss_hc36_klinikdm_05_cta.jpg",
]
MEDIA_FILES = [u.split("/")[-1] for u in MEDIA_URLS]
IMG_DIR = "/home/mantapman/.openclaw/workspace-atun/openclaw-img"

THREADS_BASE = """Klinik tidak menjual produk yang bisa dilihat di foto — klinik menjual kepercayaan.

Perjalanan calon pasien: sadar ada masalah → cari solusi lewat artikel → cari penyedia "terdekat" → memutuskan dari ulasan & kemudahan.

Di tahap "terdekat" dan memutuskan, hasil peta Google menentukan. Klinik yang tidak muncul di sana kalah, meski artikelnya banyak."""

FB_BASE = """Jasa digital marketing klinik punya tantangan berbeda dengan bisnis lain: yang dijual bukan produk, tapi kepercayaan.

Calon pasien biasanya melewati 4 tahap: sadar ada masalah, mencari solusi, mencari penyedia terdekat, lalu memutuskan. Di tahap ketiga dan keempat, hasil peta Google (map pack) muncul di atas hasil biasa — klinik yang tidak ada di sana kalah, meski artikelnya banyak.

5 strategi yang kami pakai untuk klinik & rumah sakit:
1. Konten edukasi kesehatan yang menjawab pertanyaan — langsung di awal, langkah demi langkah, plus FAQ, tetap dalam etika medis
2. Menang di pencarian "terdekat" — Google Business Profile lengkap, ulasan pasien yang segar, halaman layanan yang menyebut kota/kecamatan
3. Memudahkan cara menghubungi — tombol WhatsApp jelas, jam praktik & jadwal dokter selalu diperbarui, balasan cepat
4. Kredibilitas yang terlihat — profil dokter, penjelasan prosedur, testimoni dengan izin, kejelasan estimasi biaya
5. Konten yang bekerja jangka panjang — panduan keluhan umum, FAQ pasien, penjelasan beda layanan medis vs kosmetik

Bold statement: konten kesehatan yang tidak pernah ditinjau tenaga medis kehilangan kredibilitasnya — di mata pasien maupun mesin pencari.

Baca artikel lengkapnya: """ + ARTICLE_URL

LI_BASE = """Dulu saya mengira marketing klinik itu soal memperbanyak promosi. Ternyata bukan.

Beberapa waktu lalu kami membedah perjalanan calon pasien, dan polanya konsisten: yang dicari pasien bukan "klinik terbaik", melainkan jawaban atas masalahnya. Mereka mulai dari keluhan sederhana, membaca penjelasan, mencari penyedia terdekat, lalu memutuskan berdasarkan ulasan dan kemudahan.

Di situ saya sadar, pada tahap "terdekat" dan "memutuskan", yang paling menentukan justru hal-hal yang sering dianggap teknis: Google Business Profile yang lengkap, ulasan pasien yang segar, halaman layanan yang jelas menyebut kota, dan tombol WhatsApp yang benar-benar mudah ditemukan.

Kami menuliskannya lengkap di artikel terbaru: 5 strategi digital marketing klinik — dari konten edukasi yang menjawab pertanyaan, pencarian lokal, kemudahan menghubungi, kredibilitas yang terlihat, sampai konten jangka panjang. Termasuk kesalahan yang paling sering terjadi, seperti menjanjikan hasil instan atau mengabaikan map pack.

Pelajaran yang saya ambil: di layanan kesehatan, pemasaran yang baik bukan yang paling ramai, tapi yang paling dipercaya — akurat, mudah dihubungi, dan transparan soal biaya.

Bacaan lengkapnya: """ + ARTICLE_URL

CTA_RE = re.compile(r"komentar|pendapat|diskusi|setuju|share|pengalaman|menurut", re.I)
# pancingan yang mengerucut ke kecantikan/skincare dibuang: artikel ini tentang klinik secara umum
BAN_RE = re.compile(r"kecantikan|skincare|kulit|jerawat", re.I)


def build(base, platform, industri, tones, limit=None, max_try=80, ban=None):
    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):
                continue
            if limit is not None and len(txt) > limit:
                continue
            if ban is not None and ban.search(txt):
                continue
            return txt
    return None


# fallback terkurasi (tone netral, relevan topik klinik) kalau pancingan engine semuanya
# mengerucut ke kecantikan/skincare (artikel ini tentang klinik secara umum)
CURATED = [
    "Menurut Anda, apa hal pertama yang Anda perhatikan sebelum memilih klinik? Yuk diskusi di komentar.",
    "Menurut Anda, ulasan pasien atau rekomendasi teman yang lebih menentukan? Share pengalaman Anda di komentar.",
    "Kalau Anda menemukan klinik lewat Google, apa yang membuat Anda yakin? Yuk cerita di komentar.",
]


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


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

REPLY1 = ("Baca selengkapnya: %s\n\nButuh bantuan agar klinik Anda mudah ditemukan pasien?\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("  ❌ %s: gagal disusun" % name)
        return False
    if limit and len(txt) > limit:
        print("  ❌ %s: %d char > %d" % (name, len(txt), limit))
        ok = False
    m = re.findall(r"@\w+", txt)
    if m:
        print("  ❌ %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("  ❌ %s: hashtag KAPITAL %s" % (name, caps))
        ok = False
    if require_cta and not CTA_RE.search(txt):
        print("  ❌ %s: tidak ada pancingan diskusi" % name)
        ok = False
    print("  %s %s: %d char" % ("✅" if ok else "⚠️", name, len(txt)))
    return ok


def pil_ok():
    from PIL import Image
    ok = True
    for f in MEDIA_FILES:
        p = os.path.join(IMG_DIR, f)
        try:
            Image.open(p).verify()
            im = Image.open(p)
            print("  ✅ %s %s %s" % (f, im.size, im.mode))
        except Exception as e:
            print("  ❌ %s: %s" % (f, e))
            ok = False
    return ok


def ingress_ips():
    ips = subprocess.run(["dig", "+short", "mantapserver.tail3947bb.ts.net", "A", "@1.1.1.1"],
                         capture_output=True, text=True).stdout.split()
    return [i for i in ips if i.count(".") == 3]


def verify_urls_internet():
    ips = ingress_ips()
    ok = True
    for u in MEDIA_URLS:
        for ip in ips[:2]:
            r = subprocess.run(["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}",
                                "--max-time", "25", "--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_article():
    r = subprocess.run(["curl", "-sIL", "--http1.1", "--max-time", "200",
                        "-A", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", ARTICLE_URL,
                        "-o", "/dev/null", "-w", "%{http_code}"],
                       capture_output=True, text=True)
    code = (r.stdout or "").strip()
    print("  langsung: %s" % code)
    if code != "200":
        r2 = subprocess.run(["curl", "-s", "--max-time", "120", "-o", "/dev/null", "-w", "%{http_code}",
                             "https://r.jina.ai/" + ARTICLE_URL], capture_output=True, text=True)
        print("  via r.jina.ai: %s" % (r2.stdout or "").strip())
        code = (r2.stdout or "").strip()
    return code == "200"


def scan(needles, statuses, accounts=None):
    hits = []
    accounts = accounts or (ACC.STUDIOSOFT_THREADS, ACC.STUDIOSOFT_FB, ACC.STUDIOSOFT_LINKEDIN)
    page = 1
    while True:
        d = c.get("/public/schedule?page=%d&limit=100" % page)
        for s in d.get("docs", []):
            if s.get("accountId") not in accounts:
                continue
            if (s.get("status") or "").lower() not in statuses:
                continue
            blob = (s.get("description") or "") + (s.get("title") or "") + \
                   " ".join((m.get("url") or "") for m in (s.get("medias") or []))
            if any(n in blob for n in needles):
                hits.append((s.get("accountId"), (s.get("scheduleAt") or "")[:16],
                             (s.get("title") or "")[:45], s.get("type"),
                             len(s.get("medias") or []), len(s.get("replies") or [])))
        if page >= (d.get("totalPages") or 1):
            break
        page += 1
    return hits


def slot_busy_us(aids, slot_utc):
    busy = set()
    page = 1
    while True:
        d = c.get("/public/schedule?page=%d&limit=100" % page)
        for s in d.get("docs", []):
            if s.get("accountId") in aids and (s.get("status") or "").lower() in ("pending", "scheduled", "draft"):
                st = (s.get("scheduleAt") or "")[:19]
                if st:
                    busy.add((s.get("accountId"), st))
        if page >= (d.get("totalPages") or 1):
            break
        page += 1
    base = datetime.fromisoformat(slot_utc.replace("Z", "+00:00"))
    clash = []
    for off in range(6):
        t = (base + timedelta(minutes=off)).strftime("%Y-%m-%dT%H:%M:%S")
        for a in aids:
            if (a, t) in busy:
                clash.append((a, t))
    return clash


def next_free_09(aids):
    base = datetime.fromisoformat(SLOT_UTC.replace("Z", "+00:00"))
    for day in range(0, 12):
        cand = (base + timedelta(days=day)).strftime("%Y-%m-%dT02:00:00.000Z")
        if not slot_busy_us(aids, cand):
            return cand
    return 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 = set()
    page = 1
    while True:
        d = c.get("/public/schedule?page=%d&limit=100" % page)
        for s in d.get("docs", []):
            if s.get("accountId") == ACC.STUDIOSOFT_LINKEDIN and (s.get("status") or "").lower() in ("pending", "scheduled", "draft"):
                busy.add((s.get("scheduleAt") or "")[:19])
        if page >= (d.get("totalPages") or 1):
            break
        page += 1
    for day in range(0, 45):
        for label, uh, shift in SLOTS:
            d0 = datetime(2026, 9, 14, tzinfo=timezone.utc) + timedelta(days=day + shift)
            cand = "%sT%02d:00:00" % (d0.strftime("%Y-%m-%d"), uh)
            if cand not in busy:
                lab = (datetime.strptime(cand, "%Y-%m-%dT%H:%M:%S") + timedelta(hours=7)).strftime("%d %b %Y %H:%M WIB")
                return cand + ".000Z", lab
    return None, None


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

    print("\n1) SLOT CAROUSEL 09:00 WIB (Threads+FB):")
    aids = [ACC.STUDIOSOFT_THREADS, ACC.STUDIOSOFT_FB]
    clash = slot_busy_us(aids, SLOT_UTC)
    if clash:
        print("  ⚠️ %s terisi: %s" % (SLOT_LABEL, clash))
        cand = next_free_09(aids)
        if not cand:
            print("  ❌ tidak ada slot 09:00 bebas — batal.")
            return False
        SLOT_UTC = cand
        SLOT_LABEL = (datetime.fromisoformat(cand.replace("Z", "+00:00"))
                      + timedelta(hours=7)).strftime("%d %b %Y 09:00 WIB")
    print("  ✅ slot carousel: %s (%s)" % (SLOT_LABEL, SLOT_UTC))

    print("\n1b) SLOT LINKEDIN (text) kosong pertama:")
    cand, label = next_free_li()
    if cand:
        LI_SLOT_UTC, LI_SLOT_LABEL = cand, label
    print("  ✅ slot LI: %s (%s)" % (LI_SLOT_LABEL, LI_SLOT_UTC))

    print("\n2) CAPTION:")
    ok = True
    ok &= check_caption("Threads", THREADS, 500)
    ok &= check_caption("FB", FB)
    ok &= check_caption("LinkedIn", LI)
    ok &= check_caption("Reply1", REPLY1, require_cta=False)
    ok &= check_caption("Reply2", REPLY2, require_cta=False)
    if not ok:
        print("\n❌ caption gagal aturan — batal.")
        return False
    print("\n--- THREADS (%d char) ---\n%s" % (len(THREADS), THREADS))
    print("\n--- LINKEDIN (%d char) ---\n%s" % (len(LI), LI))

    print("\n3) IDEMPOTENSI (media hc36 sudah terjadwal?):")
    hits = scan(MEDIA_FILES[:1], ("pending", "scheduled", "draft", "success"))
    if hits:
        print("  ⚠️ sudah ada jadwal hc36 -> SKIP create:")
        for h in hits:
            print("   ", h)
        return True
    print("  ✅ belum ada jadwal hc36")

    print("\n4) VALIDASI FILE JPEG (lokal):")
    if not pil_ok():
        print("  ❌ file JPEG rusak — batal.")
        return False

    print("\n5) MEDIA VIA JALUR INTERNET (kedua ingress):")
    if not verify_urls_internet():
        print("  ❌ ada media tidak 200 — batal.")
        return False

    print("\n6) LINK ARTIKEL:")
    if not verify_article():
        print("  ❌ link artikel tidak 200 — 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 — THREADS:")
    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"] = True
    except Exception as e:
        print("  GAGAL %s" % e)
        results["threads"] = False

    print("\n   FACEBOOK:")
    try:
        alb = c.schedule_album(ACC.STUDIOSOFT_FB, FB, SLOT_UTC, medias, title=TITLE)
        pid = alb.get("scheduleId") or alb.get("_id") or alb.get("id")
        print("  album -> %s (replies kosong)" % pid)
        results["fb"] = True
    except Exception as e:
        print("  GAGAL %s" % e)
        results["fb"] = False

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

    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 (re-list):")
        for h in scan(MEDIA_FILES[:1], ("pending",)):
            print("   ", h)
        for h in scan([ARTICLE_URL.rstrip("/")], ("pending",), accounts=(ACC.STUDIOSOFT_LINKEDIN,)):
            print("    LI:", h)
        st = json.loads(open("/home/mantapman/.openclaw/workspace-atun/studiosoft_healthcare_state.json").read())
        st["index"] = st.get("index", 31) + 1
        st["topic"] = TITLE
        st["last_post"] = wib.strftime("%Y-%m-%d %H:%M")
        with open("/home/mantapman/.openclaw/workspace-atun/studiosoft_healthcare_state.json", "w") as f:
            json.dump(st, f)
        with open("/home/mantapman/.openclaw/workspace-atun/studiosoft_healthcare_log.txt", "a") as f:
            f.write("\n[%s WIB] hc36 (ARTIKEL BARU) | %s | %s | Threads+FB album %s (%s) + 2 replies Threads | "
                    "LinkedIn TEXT %s (%s) | catatan: artikel baru studiosoft.id publish 13 Sep 2026 07:42 WIB "
                    "(healthcare/klinik), 0 hit di 3.909 jadwal Repliz -> konten baru; 5 slide dibuat dari isi artikel; "
                    "semua media 200 via KEDUA ingress + PIL; state index -> %s. Sisa kandidat RE-AIR yang belum "
                    "pernah tayang: hc20 (gagal 2x 'media could not be fetched').\n"
                    % (wib.strftime("%Y-%m-%d %H:%M"), TITLE, ARTICLE_URL, SLOT_LABEL,
                       SLOT_UTC[11:16] + "Z", LI_SLOT_LABEL, LI_SLOT_UTC, st["index"]))
        print("Log + state ditulis.")
    return ok


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