#!/usr/bin/env python3
"""
portal-check.py v3 — watchdog status login job portal (CDP LANGSUNG, tanpa browser-harness).

Sumber daftar portal: BE (tabel `portals`) via `GET /v1/agent/portals` (X-Internal-Token).
Fallback: portals.json (cache) → PORTALS bawaan (offline).

Untuk tiap portal: baca cookie browser via CDP (Network.getCookies), bandingkan
dengan accounts.json, update status + sync ke BE bila berubah (POST /v1/agent/portal-status).
Silent saat tidak ada perubahan / browser mati (pola watchdog).

Menambah portal baru = INSERT di tabel `portals` (BE) — TANPA edit kode mesin.
Requires: /opt/data/venv-cdp (websocket-client). Jalankan via venv-cdp python.
"""
import json
import subprocess
import sys
import urllib.error
import urllib.request
from datetime import datetime, timezone
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent
ACCOUNTS = BASE_DIR / "accounts.json"
PORTALS_CACHE = BASE_DIR / "portals.json"
LOG_FILE = BASE_DIR / "portal-check.log"
CDP_HTTP = "http://127.0.0.1:9222"

# Fallback bawaan (dipakai kalau BE & cache tidak ada)
BUILTIN_PORTALS = {
    "jobstreet": {"urls": ["https://id.jobstreet.com/"], "checks": [("name_contains_value", "is.authenticated", "true"), ("name", "appSession")]},
    "linkedin": {"urls": ["https://www.linkedin.com/"], "checks": [("name", "li_at")]},
    "indeed": {"urls": ["https://id.indeed.com/"], "checks": [("name", "USER")]},
    "glints": {"urls": ["https://glints.com/id/"], "checks": []},
}


def log(msg, stdout=False):
    line = f"{datetime.now(timezone.utc).strftime('%F %T')} UTC {msg}"
    with open(LOG_FILE, "a") as f:
        f.write(line + "\n")
    if stdout:
        print(line, flush=True)


def load_cfg():
    cfg = {}
    try:
        for line in (BASE_DIR / "agent-config.env").read_text().splitlines():
            line = line.strip()
            if line and "=" in line and not line.startswith("#"):
                k, _, v = line.partition("=")
                cfg[k.strip()] = v.strip()
    except FileNotFoundError:
        pass
    return cfg


def fetch_portals(cfg):
    """Ambil daftar portal dari BE. Return None kalau gagal."""
    if not cfg.get("BE_API_URL") or not cfg.get("BE_INTERNAL_TOKEN"):
        return None
    url = cfg["BE_API_URL"].rstrip("/") + "/v1/agent/portals"
    req = urllib.request.Request(url, headers={"X-Internal-Token": cfg["BE_INTERNAL_TOKEN"]})
    try:
        with urllib.request.urlopen(req, timeout=8) as r:
            data = json.loads(r.read().decode() or "{}")
            if isinstance(data, dict) and "data" in data and isinstance(data["data"], dict):
                data = data["data"]
        return _normalize(data.get("portals", []))
    except Exception as e:
        log(f"fetch portals gagal: {e}")
        return None


def _normalize(portals):
    out = {}
    for p in portals:
        if not p.get("slug") or not p.get("enabled", True):
            continue
        marker_type = p.get("marker_type", "name")
        marker_name = p.get("marker_name")
        if not marker_name:
            checks = []
        elif marker_type == "name_contains_value":
            checks = [("name_contains_value", marker_name, p.get("marker_value", "true"))]
        else:
            checks = [("name", marker_name)]
        out[p["slug"]] = {"urls": [p.get("url") or f"https://{p.get('domain')}/"], "checks": checks}
    return out


def load_portals(cfg):
    fresh = fetch_portals(cfg)
    if fresh is not None and fresh:
        PORTALS_CACHE.write_text(json.dumps(fresh, indent=2))
        return fresh, "BE"
    if PORTALS_CACHE.exists():
        try:
            cached = json.loads(PORTALS_CACHE.read_text())
            if cached:
                return cached, "cache"
        except Exception:
            pass
    return BUILTIN_PORTALS, "builtin"


def run_harness(portals):
    """Baca cookie semua portal via CDP LANGSUNG (WebSocket), satu koneksi."""
    import websocket

    tabs = json.load(urllib.request.urlopen(CDP_HTTP + "/json", timeout=5))
    pages = [t for t in tabs if t["type"] == "page"]
    if not pages:
        return None
    ws_url = pages[0]["webSocketDebuggerUrl"]
    ws = websocket.create_connection(ws_url, timeout=30)
    states = {}
    try:
        _id = 0
        for portal, cfg in portals.items():
            _id += 1
            ws.send(json.dumps({
                "id": _id,
                "method": "Network.getCookies",
                "params": {"urls": cfg["urls"]},
            }))
            resp = None
            while True:
                msg = json.loads(ws.recv())
                if msg.get("id") == _id:
                    resp = msg.get("result", {})
                    break
            names_vals = {c["name"]: c.get("value", "") for c in resp.get("cookies", [])}
            if not cfg["checks"]:
                states[portal] = None
                continue
            hit = False
            for chk in cfg["checks"]:
                if chk[0] == "name" and chk[1] in names_vals:
                    hit = True
                elif chk[0] == "name_contains_value" and any(
                    chk[1] in n and v == chk[2] for n, v in names_vals.items()
                ):
                    hit = True
            states[portal] = "login" if hit else "logout"
    finally:
        try:
            ws.close()
        except Exception:
            pass
    return states


def load_accounts():
    try:
        return json.loads(ACCOUNTS.read_text())
    except Exception:
        return {}


def save_accounts(accounts):
    ACCOUNTS.write_text(json.dumps(accounts, indent=2, ensure_ascii=False))


def set_status(portal, status, accounts, cfg):
    """Update accounts.json + sync ke BE. Return (ok, out)."""
    entry = accounts.setdefault(portal, {})
    entry["status"] = status
    if status == "connected":
        entry.setdefault("connected_at", datetime.now(timezone.utc).isoformat())
    else:
        entry.pop("connected_at", None)
    entry["verified_at"] = datetime.now(timezone.utc).isoformat()
    save_accounts(accounts)
    # sync ke BE
    if cfg.get("BE_API_URL") and cfg.get("BE_INTERNAL_TOKEN"):
        url = cfg["BE_API_URL"].rstrip("/") + "/v1/agent/portal-status"
        body = json.dumps({
            "user_id": cfg.get("USER_ID", ""),
            "portal": portal,
            "status": status,
            "verified_at": entry["verified_at"],
        }).encode()
        req = urllib.request.Request(url, data=body, method="POST",
                                     headers={"Content-Type": "application/json",
                                              "X-Internal-Token": cfg["BE_INTERNAL_TOKEN"]})
        try:
            with urllib.request.urlopen(req, timeout=10) as r:
                return True, f"synced ke BE (HTTP {r.status})"
        except urllib.error.HTTPError as e:
            return False, f"BE HTTP {e.code}: {e.read().decode()[:120]}"
        except Exception as e:
            return False, f"sync gagal: {e}"
    return True, "local only (agent-config.env belum diisi)"


def main():
    cfg = load_cfg()
    portals, source = load_portals(cfg)
    log(f"daftar portal: {source} ({len(portals)} portal)")
    try:
        states = run_harness(portals)
    except Exception as e:
        log(f"browser tidak bisa dibaca ({e}) — skip")
        return 0
    if states is None:
        log("tidak ada tab/page di browser — skip")
        return 0
    accounts = load_accounts()
    changed = 0
    for portal, state in states.items():
        if state is None:
            continue
        cur = accounts.get(portal, {}).get("status")
        target = "connected" if state == "login" else "expired"
        if cur == target:
            continue
        ok, out = set_status(portal, target, accounts, cfg)
        log(f"{portal}: {cur or '-'} → {target} ({'OK' if ok else 'GAGAL'}): {out[:120]}", stdout=True)
        changed += 1
    if changed == 0:
        log("tidak ada perubahan status")
    return 0


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