#!/usr/bin/env python3
"""
Schedule konten Threads dari file JSON — VERSI STANDALONE (FIXED 1-2 Sep 2026).

Aturan Bos:
- TANPA reply/mention antar akun: setiap akun posting 1 post mandiri (standalone),
  replies=[], tidak ada replyTo, tidak ada @mention.
- WAJIB ada link artikel website sendiri di caption (sudah dicek saat generate).
- Slot 7 per hari: 05/08/11/14/17/20/23 WIB. Kalau slot yang diminta sudah terisi
  jadwal pending akun itu, geser ke slot kosong berikutnya (rule universal 1 Sep).
- Anti-redundant: URL yang dipakai dicatat ke log_threads_harian.json
  (key threads_used_links per tanggal).

Usage:
    python3 schedule_all_threads.py [content_file.json]

content_file.json format (baru, list):
{
    "date": "2026-09-03",
    "accounts": [
        {"account": "tsc", "slot": 20, "text": "caption ... (wajib ada link)"},
        {"account": "das", "slot": 23, "text": "caption ..."}
    ]
}
"""
import json, os, re, sys
import time
from datetime import datetime, timedelta, timezone

sys.path.insert(0, "/home/mantapman/.hermes/profiles/atun/workspace")
from repliz_client import get_client, ACC

CONTENT_FILE = sys.argv[1] if len(sys.argv) > 1 else "/home/mantapman/.hermes/profiles/atun/workspace/threads_content_today.json"
LOG_FILE = "/home/mantapman/.hermes/profiles/atun/workspace/log_threads_harian.json"
WIB = timezone(timedelta(hours=7))
SLOTS = [5, 8, 11, 14, 17, 20, 23]

client = get_client()

ACCOUNT_MAP = {
    "studiosoft": {"id": ACC.STUDIOSOFT_THREADS, "name": "StudioSoft"},
    "uky": {"id": ACC.UKY_THREADS, "name": "@ukyhandoko"},
    "tslaw": {"id": ACC.TSLAW_THREADS, "name": "Tslaw"},
    "tsc": {"id": ACC.TSC_THREADS, "name": "TSC / DNI Clinic"},
    "das": {"id": ACC.DAS_THREADS, "name": "DAS Bengkel Indonesia"},
}

URL_RE = re.compile(r"https?://\S+")
MENTION_RE = re.compile(r"@\w+")

# Domain website sendiri per akun — dipakai guard anti-dobel (lihat PENGECUALIAN di main()).
OWN_DOMAIN = {
    "studiosoft": "studiosoft.id",
    "uky": "studiosoft.id",
    "tslaw": "tslaw.id",
    "tsc": "theskincodeclinic.com",
    "das": "dasindonesia.com",
}

# Link HOMEPAGE saja (mis. `https://tslaw.id`) BUKAN link artikel. Guard harus menghitung
# hanya URL yang punya path setelah domain, kalau tidak post 09:00 "Mitos atau Fakta" (yang
# cuma menyebut homepage/konsultasi) bikin slot artikel harian ter-skip.
# Bukti 13, 17 & 19 Okt 2026: guard lama men-skip tslaw → harus override manual tiap kali.
OWN_ARTICLE_RE = {
    k: re.compile(r"https?://(?:www\.)?" + re.escape(v) + r"/[^\s)\]\"'>]+", re.I)
    for k, v in OWN_DOMAIN.items()
}


def load_log():
    if os.path.exists(LOG_FILE):
        with open(LOG_FILE) as f:
            return json.load(f)
    return {"last_date": "", "posts": [], "dates": [], "failed": []}


def save_log(log):
    with open(LOG_FILE, "w") as f:
        json.dump(log, f, indent=2, ensure_ascii=False)


def fetch_all_schedules():
    """SEMUA jadwal (semua status) — retry keras, JANGAN break diam-diam saat API flaky.

    PITFALL 21 Sep 2026: versi lama (fetch_all_pending) hanya mengambil pending/draft dan
    `break` pada exception pertama → API 500 bikin daftar tidak lengkap, slot yang SUDAH
    tayang dianggap kosong, akun (SS/uky) yang queue-nya penuh ikut terjadwal di jam yang
    sudah terpakai (dobel post). Wajib: retry + pakai SEMUA status + konversi ke WIB.
    """
    all_docs = []
    for p in range(1, 150):
        got = None
        for i in range(15):
            try:
                got = client.get_schedules(page=p, limit=100)
                break
            except Exception as e:
                print(f"    ⏳ page {p} gagal ({str(e)[:60]}), retry {i+1}/15")
                time.sleep(min(40, 3 + 4 * i))
        if got is None:
            raise RuntimeError(f"Gagal mengambil halaman {p} setelah 15 percobaan")
        batch = got.get("docs", [])
        if not batch:
            break
        all_docs.extend(batch)
        if len(batch) < 100:
            break
    print(f"  📚 {len(all_docs)} jadwal terbaca dari API (semua status)")
    return all_docs


def wib_key(schedule_at):
    """'2026-09-21T10:00:00.000Z' → ('2026-09-21', 17) dalam WIB, atau None."""
    if not schedule_at:
        return None
    try:
        dt = datetime.fromisoformat(str(schedule_at).replace("Z", "+00:00")).astimezone(WIB)
    except Exception:
        return None
    return (dt.strftime("%Y-%m-%d"), dt.hour)


def utc_ts(dt_wib):
    return dt_wib.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z")


def main():
    if not os.path.exists(CONTENT_FILE):
        print(f"❌ File {CONTENT_FILE} tidak ditemukan!")
        sys.exit(1)

    data = json.load(open(CONTENT_FILE))
    date_str = data.get("date", "unknown")
    accounts = data.get("accounts", [])
    if isinstance(accounts, dict):
        # toleransi format lama dict: konversi
        accounts = [{"account": k, "slot": v.get("slot", 8), "text": (v.get("posts") or [""])[0]} for k, v in accounts.items()]
    if not accounts:
        print("❌ Tidak ada konten di file!")
        sys.exit(1)

    print(f"📅 SCHEDULE THREADS (STANDALONE) — {date_str}")
    print("=" * 64)

    parts = date_str.split("-")
    year, month, day = int(parts[0]), int(parts[1]), int(parts[2])
    today = datetime(year, month, day, tzinfo=WIB)
    now = datetime.now(WIB)

    sched_map = {}
    for d in fetch_all_schedules():
        sched_map.setdefault(d.get("accountId"), []).append(d)

    results = []
    used_links = {}
    for item in accounts:
        key = item.get("account")
        if key not in ACCOUNT_MAP:
            print(f"  ⚠️  Akun '{key}' tidak dikenal, skip")
            continue
        acc_info = ACCOUNT_MAP[key]
        text = (item.get("text") or "").strip()
        if not text:
            print(f"  ⚠️  {acc_info['name']}: caption kosong, skip")
            continue
        if MENTION_RE.search(text):
            print(f"  🚫 {acc_info['name']}: caption mengandung @mention — TIDAK BOLEH, skip")
            continue
        links = URL_RE.findall(text)
        if not links:
            print(f"  🚫 {acc_info['name']}: caption TANPA link artikel — TIDAK BOLEH, skip")
            continue

        want_hour = int(item.get("slot", 0))
        acc_docs = sched_map.get(acc_info["id"], [])

        # 🔒 Aturan Bos: maksimal 1 konten per akun per hari — cek SEMUA status,
        # bukan hanya pending (post yang sudah tayang hari itu tetap terhitung).
        # 🔓 PENGECUALIAN 22 Sep 2026 (opt-in per item: "allow_if_no_ownlink"): kalau jadwal
        # same-day itu datang dari PIPELINE LAIN dan TIDAK memuat link website sendiri
        # (mis. image TSC tanpa link, video-short DAS ke YouTube), post thread artikel-link
        # tetap dibuat — mandat harian "tiap akun punya 1 post ber-link website sendiri"
        # tidak boleh mati hanya karena pipeline lain mengisi jam 09:00/17:10.
        # Tanpa flag ini perilaku lama berlaku: ada jadwal same-day → SKIP (tidak digeser).
        same_day = [d for d in acc_docs if (wib_key(d.get("scheduleAt")) or ("", -1))[0] == date_str]
        if same_day:
            own = OWN_DOMAIN.get(key)
            # Perbaikan 16 Okt 2026: hitung HANYA link yang benar-benar URL (http...).
            # Penyebutan domain di teks (mis. "Konsultasi: tslaw.id" atau mention "@studiosoft.id")
            # BUKAN link artikel, jadi jangan sampai slot artikel harian ter-skip karenanya
            # (kasus 13 Okt 2026: guard lama salah hitung → terpaksa override manual).
            # Perbaikan 19 Okt 2026: URL homepage tanpa path (mis. `https://tslaw.id`) juga
            # BUKAN link artikel → pakai OWN_ARTICLE_RE (wajib ada path setelah domain).
            own_re = OWN_ARTICLE_RE.get(key)
            has_ownlink = any(own and own_re.search(d.get("description") or "") for d in same_day)
            if item.get("allow_if_no_ownlink") and not has_ownlink:
                print(f"  ⚠️  {acc_info['name']}: {len(same_day)} jadwal lain pada {date_str} "
                      f"TANPA link {own} → lanjut buat post artikel (opt-in allow_if_no_ownlink)")
            else:
                print(f"  ⛔ {acc_info['name']}: sudah ada {len(same_day)} jadwal pada {date_str} "
                      f"(maks 1/hari, semua status dihitung) → SKIP, tidak digeser")
                results.append({"account": key, "status": "already_scheduled_today", "count": len(same_day)})
                continue

        taken = {wib_key(d.get("scheduleAt")) for d in acc_docs}
        chosen = None
        for h in [want_hour] + [s for s in SLOTS if s != want_hour]:
            cand = today.replace(hour=h, minute=0, second=0, microsecond=0)
            if cand <= now:
                continue
            if (date_str, h) not in taken:
                chosen = cand
                break
        if chosen is None:
            print(f"  ⚠️  {acc_info['name']}: tidak ada slot kosong, skip")
            results.append({"account": key, "status": "no_free_slot"})
            continue

        at = utc_ts(chosen)
        print(f"\n📝 {acc_info['name']} → {chosen.strftime('%Y-%m-%d %H:%M')} WIB (UTC {at})")
        try:
            r = client.create_schedule(
                acc_info["id"], text, at,
                title=item.get("title") or f"{acc_info['name']} - Daily Thread",
                topic=item.get("topic") or "Daily Threads",
                replies=[],          # WAJIB: standalone, tanpa reply
            )
            sid = r.get("scheduleId") or r.get("_id") or r.get("id") or "?"
            print(f"  ✅ create response: {sid}")
            results.append({"account": key, "slot": chosen.hour, "status": "ok", "id": sid, "scheduleAt": at})
            used_links.setdefault(key, []).append({"url": links[0], "slot": chosen.hour, "scheduleAt": at})
        except Exception as e:
            print(f"  ❌ {e}")
            results.append({"account": key, "slot": chosen.hour, "status": "error", "error": str(e)})

    # ── Verifikasi dari API (jangan percaya response create saja) ──
    print("\n" + "=" * 64)
    print("🔎 VERIFIKASI dari API (cari per _id + jam WIB, semua status)...")
    all2 = fetch_all_schedules()
    by_id = {d.get("_id"): d for d in all2}
    for res in results:
        if res["status"] != "ok":
            continue
        acc_info = ACCOUNT_MAP[res["account"]]
        d = by_id.get(res.get("id"))
        if d is None:
            k = wib_key(res["scheduleAt"])
            found = [x for x in all2 if x.get("accountId") == acc_info["id"]
                     and wib_key(x.get("scheduleAt")) == k]
            d = found[0] if found else None
        if d is not None:
            desc = d.get("description") or ""
            reps = d.get("replies") or []
            ok_ment = "OK" if not MENTION_RE.search(desc) else "ADA @ !!"
            ok_link = "OK" if URL_RE.search(desc) else "HILANG !!"
            print(f"  ✅ {acc_info['name']} {res['scheduleAt']} id={d.get('_id')} "
                  f"status={d.get('status')} len={len(desc)} mention={ok_ment} link={ok_link} "
                  f"replies={len(reps) if isinstance(reps, list) else '?'}")
        else:
            print(f"  ⚠️  {acc_info['name']}: jadwal {res['scheduleAt']} (id {res.get('id')}) "
                  f"TIDAK ditemukan di API — cek manual!")

    # ── Log history (anti-redundant) ──
    log = load_log()
    log["last_date"] = date_str
    if not isinstance(log.get("posts"), list):
        log["posts"] = []
    log["posts"].append({"date": date_str, "threads_standalone": results})
    if used_links:
        # Gabung, jangan timpa: kalau tanggal yang sama dijalankan dua kali dalam sehari,
        # catatan link dari jalan pertama tidak boleh hilang (perbaikan 16 Okt 2026).
        day_links = log.setdefault("threads_used_links", {}).setdefault(date_str, {})
        for acc_key, items in used_links.items():
            day_links.setdefault(acc_key, []).extend(items)
    save_log(log)

    print("\n" + "=" * 64)
    oks = [r for r in results if r["status"] == "ok"]
    errs = [r for r in results if r["status"] != "ok"]
    print(f"✅ SCHEDULE SELESAI! {len(oks)} sukses | {len(errs)} skip/gagal")
    print(f"   Link dipakai {date_str}: {json.dumps(used_links, ensure_ascii=False)}")
    print("=" * 64)


if __name__ == "__main__":
    main()
